The upcoming generated types will match those for pseudo-classes: A
PseudoElementSelector type, that then holds a PseudoElement enum
defining what it is. That enum will be at the top level in the Web::CSS
namespace.
In order to keep the diffs clearer, this commit renames and moves the
types, and then a following one will replace the handwritten enum with
a generated one.
If an element is affected only by selectors using the direct sibling
combinator `+`, we can calculate the maximum invalidation distance and
use it to limit style invalidation. For example, the selector
`.a + .b + .c` has a maximum invalidation distance of 2, meaning we can
skip invalidating any element affected by this selector if it's more
than two siblings away from the element that triggered the style
invalidation.
This change results in visible performance improvement when hovering
PR list on GitHub.
12c6ac78e2 with fixed mistake when cache
slot is copied instead of being referenced:
```cpp
auto cache =
box.cached_intrinsic_sizes().min_content_height.ensure(width);
```
while it should've been:
```cpp
auto& cache =
box.cached_intrinsic_sizes().min_content_height.ensure(width);
```
This change moves intrinsic sizes cache from
LayoutState, which is local to current layout run,
to layout nodes, so it could be reused between
layout runs. This optimization is possible because
we can guarantee that these measurements will
remain unchanged unless the style of the element
or any of its descendants changes.
For now, invalidation is implemented simply by
resetting cache on whole ancestors chain once we
figured that element needs layout update.
The case when layout is invalidated by DOM's
structural changes is covered by layout tree
invalidation that drops intrinsic sizes cache
along with layout nodes.
I measured improvement on couple websites:
- Mail list on GMail 28ms -> 6ms
- GitHub large code page 47ms -> 36ms
- Discord chat history 15ms -> 8ms
(Time does not include `commit()`)
This allows us to avoid a full layout tree rebuild after change of
"display" property, which happens frequently in practice. It also
allows us to avoid a full rebuild after DOM node insertion, since
previously, computing styles for newly inserted nodes would trigger a
complete layout tree rebuild.
We already have logic to play or cancel animations in an element's
subtree when the display property changes to or from none. However,
this was not sufficient to cover the case when an element starts/stops
being nested in display none after insertion.
The DOMParsing spec is in the process of being merged into the HTML one,
gradually. The linked spec change moves XMLSerializer, but many of the
algorithms are still in the DOMParsing spec so I've left the links to
those alone.
I've done my best to update the GN build but since I'm not actually
using it, I might have done that wrong.
Corresponds to 2edb8cc7ee
Instead of marking all nodes in the subtree for style recalculation,
including subtrees of subsequent siblings, we can fall back to the
default invalidation path, which is optimized to skip siblings
unaffected by sibling selectors.
Makes scrolling on https://frame.work/pl/en/about go a lot smoother.
This reduces the number of `.cpp` files that need to be recompiled when
one of the below header files changes as follows:
CSS/ComputedProperties.h: 1113 -> 49
CSS/ComputedValues.h: 1120 -> 209
Instead of checking all elements in a document for containment in
`:has()` invalidation set, we could narrow this down to ancestors and
ancestor siblings, like we already do for subject `:has()` invalidation.
This change brings great improvement on GitHub that has selectors with
non-subject `:has()` and sibling combinators (e.g., `.a:has(.b) ~ .c`)
which prior to this change meant style invalidation for whole document.
This commit changes the strategy for updating inherited styles. Instead
of marking all potentially affected nodes during style invalidation, the
decision is now made on-the-fly during style recalculation. Child nodes
will only have their inherited styles recalculated if their parent's
properties have changed.
On Discord this allows to 1000x reduce number of nodes with recalculated
inherited style.
The current implementation of `:has()` style invalidation is divided
into two cases:
- When used in subject position (e.g., `.a:has(.b)`).
- When in a non-subject position (e.g., `.a > .b:has(.c)`).
This change focuses on improving the first case. For non-subject usage,
we still perform a full tree traversal and invalidate all elements
affected by the `:has()` pseudo-class invalidation set.
We already optimize subject `:has()` invalidations by limiting
invalidated elements to ones that were tested against `has()` selectors
during selector matching. However, selectors like `div:has(.a)`
currently cause every div element in the document to be invalidated.
By modifying the invalidation traversal to consider only ancestor nodes
(and, optionally, their siblings), we can drastically reduce the number
of invalidated elements for broad selectors like the example above.
On Discord, when scrolling through message history, this change allows
to reduce number of invalidated elements from ~1k to ~5.
f7a3f78 made the layout tree invalidate only the inserted nodes
themselves, but it turned out that CSS containment invalidation relies
on the parent being invalidated as well.
There is no need for this invalidation because taking care of siblings
is already done by invalidation with `NodeInsertBefore` reason. Parent
element itself (without subtree) is always invalidated by
`Node::children_changed()` hook, so `:empty` pseudo-class invalidation
is already covered.
When checking whether an early return is possible because some ancestor
already has the whole subtree invalidation flag set, the check should
begin with the current node's parent rather than with the node itself.
Otherwise, if a node already has the whole subtree invalidation flag
set and is subsequently invalidated for the reason `NodeInsertBefore`
or `NodeRemove`, we will skip the sibling invalidation required for
these operations
This fix is required for optimizations in subsequent commits.
With this change, siblings of an inserted node are no longer invalidated
unless the insertion could potentially affect their style. By
"potentially affected," we mean elements that are evaluated against the
following selectors during matching:
- Sibling combinators (+ or ~)
- Pseudo-classes :first-child and :last-child
- Pseudo-classes :nth-child, :nth-last-child, :nth-of-type, and
:nth-last-of-type
Previous name for misleading because it checks if box could be scrolled
by user input event which is diffent from checking if box is scrollable.
For example box with `overflow: hidden` is scrollable but it can't be
scrolled by user input event.
Currently, this metadata is only provided on the insertion steps,
though I believe it would be useful to extend to the other cases
as well. This metadata can aid in making optimizations for these
steps by providing extra context into the type of change which
was made on the child.
...until Document::update_style(). This allows to avoid doing full
document DOM tree traversal on each Node::invalidate_style() call.
Fixes performance regression on wpt.fyi
Prior to this change, we invalidated all elements in the document if it
used any selectors with :has(). This change aims to improve that by
applying a combination of techniques:
- Collect metadata for each element if it was matched against a selector
with :has() in the subject position. This is needed to invalidate all
elements that could be affected by selectors like `div:has(.a:empty)`
because they are not covered by the invalidation sets.
- Use invalidation sets to invalidate elements that are affected by
selectors with :has() in a non-subject position.
Selectors like `.a:has(.b) + .c` still cause whole-document invalidation
because invalidation sets cover only descendants, not siblings. As a
result, there is no performance improvement on github.com due to this
limitation. However, youtube.com and discord.com benefit from this
change.
...by replacing existing method to check if an element is affected by
invalidation property. It turned out there is no need to check if an
element is affected only by some specific property, so it's more
convenient to have a method that accepts the whole set.
We have an optimization that allows us to invalidate only the style of
the element itself and mark descendants for inherited properties update
when the "style" attribute changes (unless there are any CSS rules that
use the "style" attribute, then we also invalidate all descendants that
might be affected by those rules). This optimization was not taking into
account that when the inline style has custom properties, we also need
to invalidate all descendants whose style might be affected by them.
This change fixes this bug by saving a flag in Element that indicates
whether its style depends on any custom properties and then invalidating
all descendants with this flag set when the "style" attribute changes.
Unlike font relative lengths invalidation, for elements that depend on
custom properties, we need to actually recompute the style, instead of
individual properties, because values without expanded custom properties
are gone after cascading, and it has to be done again.
The test added for this change is a version of an existing test we had
restructured such that it doesn't trigger aggressive style invalidation
caused by DOM structured changes until the last moment when test results
are printed.
Instead of traversing the entire DOM subtrees and marking nodes for
style update, this patch adds a new mechanism where we can mark a
subtree root as "entire subtree needs style update".
A new pass in Document::update_style() then takes care of coalescing
all these invalidations in a single traversal of the DOM.
This shaves *minutes* of loading time off of https://wpt.fyi/ subpages.
Instead, change the APIs from "has :foo" to "may have :foo" and return
true if we don't have a valid rule cache at the moment.
This allows us to defer the rebuilding of the rule cache until a later
time, for the cost of a wider invalidation at the moment.
Do note that if our rule cache is invalid, the whole document has
invalid style anyway! So this is actually always less work. :^)
Knocks ~1 second of loading time off of https://wpt.fyi/
Implements idea described in
https://docs.google.com/document/d/1vEW86DaeVs4uQzNFI5R-_xS9TcS1Cs_EUsHRSgCHGu8
Invalidation sets are used to reduce the number of elements marked for
style recalculation by collecting metadata from style rules about the
dependencies between properties that could affect an element’s style.
Currently, this optimization is only applied to style invalidation
triggered by class list mutations on an element.
These common cases now cause us to invalidate the layout tree starting
at the relevant parent node instead of invalidating the entire tree.
- DOM node insertion
- DOM node removal
- innerHTML setter
- textContent setter
This makes a lot of dynamic content much faster. For example, demos
on shadertoy.com go from ~18 fps to ~28 fps on my machine.
DOM nodes now have two additional flags:
- Needs layout tree update
- Child needs layout tree update
These work similarly to the needs-style-update flags, but instead signal
the need to rebuild the corresponding part of the layout tree.
When a specific DOM node needs a layout tree update, we try to create
a new subtree starting at that node, and then replace the subtree in the
old layout tree with the newly created subtree.
This required some refactoring in TreeBuilder so that we can skip over
entire subtrees during a tree update.
Note that no partial updates happen yet (as of this commit) since we
always invalidate the full layout tree still. That will change in the
next commit.
Per https://w3c.github.io/aria/#document-handling_author-errors_roles,
determining whether to ignore certain specified landmark roles requires
first determining whether the element for which the role is specified
has an accessible name.
But if we then try to retrieve a role for such elements, we end up
calling right back into the accessible-name computation code — which
would cause the calls to loop infinitely.
So to avoid that — and to have handling for any other future cases the
spec may introduce of such recursive calls that will loop indefinitely —
this change introduces a parameter that callers can pass to cause
role-attribute lookup to be skipped during accessible-name computation.
When setting the textContent of an element with no children to null or
the empty string, nothing happens. Even so, we were still invalidating
style, layout and collections, causing pointless churn.
Skipping invalidation in this case also revealed that we were missing
invalidation when changing the selected state of HTMLOptionElement.
This was all caught by existing tests already in-tree. :^)