Add a late rule-cache bucket for selectors that do not expose a
current-element bucket but do expose ancestor-filter metadata. These
rules previously stayed in the global other bucket and relied on the
ancestor filter after broad enumeration.
Visit the new buckets only when the current ancestor bloom filter may
contain the bucket hash. Keep pseudo-class invalidation caches opted
out because those caches answer targeted invalidation questions rather
than normal style collection.
On the GitHub profile, other-bucket candidates fell from about 5.50k
to about 1.07k per 1k rule collection calls.
Keep pseudo-element style rules out of the normal element rule buckets
while preserving the same id, class, tag, attribute and root buckets
inside each known pseudo-element type. This avoids collecting pseudo
rules for normal element style, without broadening pseudo style
collection to every rule targeting the queried pseudo-element.
Update the has invalidator to walk both the normal rule buckets and
the pseudo-element bucket maps when deciding whether pending :has()
mutations may affect style.
Split StyleCache's rule matching data from its invalidation metadata.
Allow callers to build either payload independently. Style invalidation
queries no longer force a full rule cache rebuild, and rule matching
no longer builds invalidation metadata as a side effect.
Previously, callers often treated an absent rule cache as proof that no
style invalidation metadata existed. That made some invalidation paths
do nothing until something else had populated the rule cache first.
Build invalidation data before reading it instead, so these paths use
the metadata whenever stylesheet rules require it.
Rebaseline style invalidation counter expectations for the new lazy
build points. Flush setup style work in the structural :has() feature
filter test before measuring each mutation, so the recomputation
counters describe the mutation itself instead of leftover setup work.
Changing :active or :open used the broad style invalidation path, so
large subtrees were recomputed even when only the target element and
selector-matched relatives could be affected.
Reuse the :has() feature collector to keep conservative fallback for
observable :has() cases, then use pseudo-class property invalidation
for the common targeted path.
Separate MediaQueryList change reporting from stylesheet media rule
invalidation. Creating matchMedia() objects evaluates their own baseline
state, but should not make the next style update walk all active
stylesheets when the media environment has not changed.
This avoids continuous stylesheet media query reevaluation during
YouTube video playback, where repeated matchMedia() creation can make
style flushes do unnecessary work.
Property invalidation inside :nth-child(... of ...) used to become a
whole-subtree invalidation plan. That is broader than needed for
property changes that only affect the filtered sibling list, such as
:has() becoming true or false for one sibling.
Add an invalidation plan bit that marks the element and structurally
affected siblings instead. Keep whole-subtree invalidation when a
stronger plan already requires it, and cover both :nth-child and
:nth-last-child filters with :has() regression tests.
Feature-filter :not() arguments in :has() when the same compound also
has a concrete tag, id, class, or attribute selector. Bare negations
still stay conservative, but anchored negations no longer make unrelated
subtree mutations walk every :has() candidate.
Also avoid installing the whole-subtree :has() fallback for rightmost
complex :is()/:where() arguments that only use descendant or child
combinators. Keep the fallback for non-rightmost and sibling-combinator
cases where the existing plan cannot represent the nested selector
context.
Add counter-based style invalidation coverage for both cases.
Record hover, focus, focus-visible, focus-within, and target pseudo
classes in :has() invalidation metadata. This lets the existing
property invalidation path schedule :has() ancestor invalidation only
for scopes whose :has() selectors mention the changed pseudo-class.
Interaction pseudo-class invalidation previously scheduled :has()
ancestor invalidation for every style scope containing any :has()
selector. That kept selectors like .wrapper:has(:focus) * correct, but
also caused unrelated hover and focus changes to fan out through broad
:has() descendant invalidation rules.
Update style invalidation coverage so unrelated hover changes avoid the
extra :has() walk, while a descendant :has(:focus) rule still restyles
its affected descendants when focus changes.
When an element was affected by :has() in subject position,
pending :has() mutation invalidation also applied the pseudo-class
invalidation plan for that element. If the same element had ever been
observed in a non-subject :has() selector, that plan could invalidate
the element's whole descendant subtree even when the mutation could
only require the subject element itself to recompute style.
Keep subject-position :has() invalidation to the element itself, and
run the descendant fanout only for non-subject involvement after the
existing mutation feature filter says that fanout may be affected.
Track the strongest invalidation applied to each anchor, so a later
batched mutation can still upgrade earlier anchor-only work to a
descendant fanout.
Extend the filter to treat known state pseudo-classes as concrete
mutation features, so a :hover mutation does not conservatively match
unrelated selectors such as :has(dialog:modal).
Scope boundary selectors are kept conservative where needed: when an
@scope boundary contains :has(), the scope root's match can activate or
deactivate style rules for descendants, so it still records descendant
involvement.
Add coverage for unrelated hover and batched mutation cases on elements
whose ancestors have both subject and non-subject :has() involvement,
ensuring descendant no-op recomputation stays bounded without dropping
required descendant fanout.
Track last-child and backward positional selector dependencies
separately on parent nodes. A last-child or only-child selector can only
change the element at the trailing edge, so insertions and removals can
invalidate that element directly instead of walking every previous
sibling.
Keep the previous-sibling walk for selectors such as nth-last-child and
last-of-type, where every previous element's from-end position may
change.
Include tag and attribute selectors in guarded pseudo-class invalidation
plans. This keeps selectors like a:hover .target from applying work to
unrelated descendants when :hover changes on other elements.
Store both original and lowercase attribute names for invalidation keys.
HTML attribute mutations still hit lowercase selector buckets, while SVG
and MathML selectors such as [viewBox] keep their case-sensitive guards.
Use the same names for :has() metadata and mutation feature filtering so
attribute changes do not get filtered out after metadata lookup.
Match tag invalidation properties against lowercased local names, like
invalidation data and rule cache buckets do. The regression tests cover
SVG hover fanout and case-sensitive :has() attribute mutation.
Scoped rules depend on their `@scope` start and end selectors, not just
the selectors for the declarations inside the rule. Register those
boundary selectors in the style invalidation data and selector insights
so mutations that create or remove scope roots or limits invalidate
descendant styles that may now match differently.
Scope boundary matching also needs to record selector involvement
against the element being styled. That keeps :has(), sibling, and
structural selector invalidation from treating the boundary candidate
as the only affected subject.
Run the stored :has() invalidation plans for affected anchors as well
as marking subject anchors dirty. This lets scope-boundary :has()
changes invalidate scoped descendants instead of leaving stale styles.
Only mark an element itself dirty for interaction pseudo-class changes
when matching rules can plausibly match that element. Check every style
scope observing the element, and keep the normal invalidation plan for
descendants and siblings.
Treat pseudo-elements as neutral during the plausibility check so rules
like a:hover::before still invalidate the originating element. Add text
coverage for non-matching hover rules and pseudo-element hover changes.
When @media rules change match state, avoid treating that as a
whole-document stylesheet change. Record the rules under the query
that became effective or ineffective, build the normal stylesheet
invalidation set from those rules, and invalidate only matching
elements. Keep broad invalidation for rule kinds whose effects are not
selector-targetable.
Preserve cascade-wide fallout by reusing stylesheet-change shadow
effect analysis for broad invalidations inside shadow roots. This keeps
:host and slotted content current when active rules in the same shadow
scope can match there.
Also report an imported stylesheet's owner rule when the imported
sheet's own media gate changes. Layered @import rules can affect layer
ordering even when the imported sheet contributes no rules, so they
need the same broad invalidation treatment as other cascade-wide rules.
Add viewport resize coverage for media query breakpoints, broad
shadow-root media invalidation, and empty layered imports whose media
gate changes layer order.
Compound selectors with a non-rightmost pseudo-class used to register
their descendant and sibling invalidation plans directly under the
pseudo-class property. A selector like `.item:hover * .target` ran
the descendant plan for every hovered element, even when that element
could not match `.item`.
Store those non-rightmost plans behind a guard made from stable class
and id subject features in the same compound selector. Deliberately
leave pseudo-classes out of guards, since one mutation can change
multiple state pseudo-classes at once. Also leave tag and attribute
selectors out, since their matching depends on document and namespace
case-sensitivity that the guard property does not carry.
The new style-invalidation test covers the GitHub-shaped
`:hover * :not(...)` case, co-invalidated `:link` and `:any-link`,
partial or complex `:is()`/`:where()` alternatives, and case-sensitive
SVG tag and attribute selectors.
Replace PseudoClassInvalidator's subtree scan with targeted
invalidation for elements whose pseudo-class state changes. This scopes
state changes to affected selectors instead of rechecking a whole
common-ancestor subtree.
Use the ancestor chain that matches each pseudo-class. Hover walks the
shadow-including chain. :focus-within walks the flat-tree chain, so
slotted content invalidates its assigned slot and relevant shadow-tree
descendants. Focus, FocusVisible, and Target invalidate just the state
node.
Route each affected element through Element::invalidate_style with the
pseudo-class property. This uses the same invalidation-plan machinery as
Disabled, Checked, and other pseudo-class state changes.
Interaction state pseudo classes are not tracked in :has() metadata, so
schedule :has() ancestor invalidation explicitly when the state flips.
The callers no longer need cross-scope branching. The chain walk handles
shadow boundaries, and property invalidation already visits every
observer style scope.
Do not invalidate shadow-root rule caches when the document user style
sheet changes. Shadow trees still need style invalidation because
document user rules can match their descendants, but their local author
rule caches do not include the document user sheet.
Since user and content-blocker sheets now live only in the document
scope, shadow-DOM state changes also have to consult the document user
selector insights for :has() and pseudo-class invalidation.
Add content blocker coverage for user-style refreshes, shadow :has()
state changes, and shadow pseudo-class invalidation.
Cache lightweight selector insights on each stylesheet so cold style
scopes can answer whether :has() invalidation is relevant without
building a complete rule cache. This avoids forcing rule caches for
shadow roots whose active sheets do not contain :has() selectors.
Imported sheets contribute to their parent sheet's effective rules, so
an imported sheet load or CSSOM change also clears ancestor selector
insight caches.
Add test-only counters and regression coverage for cold shadow roots
with and without :has() selectors, plus delayed imported :has() rules.
Do not process pending :has() mutation invalidation for shadow roots
that have no active style sheets. Such scopes cannot contain shadow
rules with :has(), and document-level user rules are handled by the
document style scope before shadow roots are visited.
Add a text test that mutates an unstyled shadow root while document
:has() invalidation is active, covering the safety boundary for this
shortcut.
Make style updates reach a fixed point when slot invalidation dirties
assigned nodes after their traversal point. During inherited-style
cascades, only the topmost changed element scans for descendant slots.
Animation inherited-style updates now include the target slot and walk
shadow-including descendants, so host animations propagate inherited
values through shadow trees and assigned slottables. Animated inherited
longhands also carry the same inherited-style invalidation signal as
regular style changes.
Mark custom elements dirty when their :defined state flips, so upgraded
elements do not keep stale :not(:defined) computed style. Add coverage
for slotted menu invalidation, descendant-slot scan counts, target slot
animations, host shadow-tree animations, and explicit inherit from an
animated non-inherited longhand.
Include ::part() rules when comparing pseudo-class rule matches during
focus state invalidation. For observer style scopes, also walk the
containing shadow roots of the old and new focused nodes so document and
ancestor shadow scopes can invalidate exported part targets.
Add coverage for direct and exported parts using :focus, :focus-visible,
and :focus-within.
Match :focus-within by walking flat-tree parents so shadow hosts and
slotted content follow the Selectors definition instead of DOM ancestry.
When focus state changes, also run pseudo-class invalidation in shadow
style scopes that can observe the focused node. This lets shadow-root
rules such as :host(:focus-within)::before and ::slotted(:focus) update
without waiting for an unrelated DOM mutation to restyle the host.
Add focused coverage for host pseudo-elements, shadow descendants, and
slotted controls, and update the style-invalidation counter baseline for
the lower focus-family no-op restyle counts.
Similar to GC::Root<T>, make GC::RootVector<T> constructible without
explicitly passing a Heap.
This is implemented by having RootVectorBase use GC::Heap::the() for
heap-free construction.
Previously, and according to the spec, `a::part(foo)::before` would be a
single CompoundSelector, even though it matches against 3 different
targets. This meant some awkward swapping of targets in the middle of
matching, and in particular it made `::part()` and `::slotted()` quite
hacky, requiring them to track extra data on the MatchContext to then
use later. This was scattered around and difficult to follow.
Partly inspired by Gecko, this commit instead introduces an invisible
PseudoElement combinator. After parsing a selector, we find any
CompoundSelectors that contain a pseudo-element and split them up, so
that each CompoundSelector only has a single target in the end. Where
the pseudo-element was at the start of a CompoundSelector, we insert an
invisible universal selector before it to represent its originating
element.
So now, a CompoundSelector deals with one target, and switching targets
is done at the combinator.
The one inconsistency is that we match the target of ::slotted()
and ::part() in pseudo_element_transition_target(), instead of before
then when processing the SimpleSelector. This is to avoid repeating the
same computations twice.
No outward-facing behaviour changes, though the invalidation metrics
have changed.
Previously, adding or removing a constructed `CSSStyleSheet` via
`document.adoptedStyleSheets` invalidated style for the entire document
or shadow root. We now route this through the same style invalidation
machinery that's used when we add or remove `<style>` elements,
resulting in fewer full invalidations.
When a sheet's rightmost compound carries :first-child, :last-child,
or :only-child but no other class/tag/id/attr feature, the sheet
add/remove path can target the boundary children directly instead of
falling back to a whole-subtree invalidation.
The narrowing in extend_style_sheet_invalidation_set_with_style_rule
deliberately only kicks in when the structural pseudo is the only
feature in the rightmost compound. A selector like `.foo:first-child`
keeps targeting `.foo` rather than every first-child in the
document.
Adds matchers for :first-child / :last-child / :only-child in
InvalidationSetMatcher so the targeted walk can recognize them on
each candidate element.
Stylesheet add/remove previously fell back to a whole-subtree
invalidation whenever a sheet's rightmost compound carried one of
these pseudo-classes. They each match a small, knowable set of
elements at any moment, so the invalidation walk can target them
directly:
- :host matches one element per shadow root and :root matches the
html element. Mark them targetable.
- :hover, :focus, :focus-visible, :focus-within, :active, and
:target all match at most a handful of elements at a time. Mark
them targetable and add matchers in InvalidationSetMatcher that
consult Document::hovered_node, focused_area, target_element,
and the element's own focused/active state.
invalidate_structurally_affected_siblings used to mark every previous
or next sibling that had ever observed :first-child, :last-child, or
:only-child. Their match result can flip for at most one element per
single mutation, so compute that transition target once and only mark
it. The :nth-child and :nth-last-child paths still re-evaluate every
affected sibling since their indices shift on every mutation.
Drops elementStyleNoopRecomputations from ~22000 to ~15000 on the
github.com/LadybirdBrowser/ladybird page (60s settle, three-run
median). Rebaselines the structural-pseudo-class-precision test, with
the :nth-child / :nth-last-child counters intentionally unchanged.
Avoid repeated :has() child-list scheduling when pending data already
covers every concrete feature bucket used by :has() selectors in a style
scope. Featureless-sensitive scopes record child-list mutations
conservatively instead.
That conservative path avoids scanning mutation subtrees for concrete
features when invalidation cannot rely on them anyway. When loading
the Intel ISA PDF in pdf.js, instrumented subtree feature-collection
visits at about 40k style invalidations dropped from around 71k to 1.6k,
saving ~650ms of main thread time on my Linux machine. :^)
Collect concrete features from pending :has() mutations and use them to
avoid re-invalidating non-subject :has() anchors whose matching rules
cannot be affected by the mutation.
Keep conservative behavior for shadow-boundary fanout, structural
sibling changes, and selectors whose relevant features cannot be proven.
For sibling-combinator relative selectors, avoid marking an anchor as
handled until it is actually invalidated, and make the sibling scan
respect anchors skipped by the feature filter for the same mutation.
Keep StyleScope responsible for storing pending :has() mutation state,
but move the invalidation walk and scheduling helpers into
CSS::Invalidation::HasMutationInvalidator. This keeps the style scope
from owning the :has() invalidation algorithm directly and gives later
changes a narrower place to optimize.
Add --dump-style-invalidation-counters=N to Ladybird and propagate it
to WebContent helper processes.
When enabled, WebContent dumps the current document style invalidation
counters with dbgln() after every N recorded style invalidations. This
makes it possible to collect the counters while browsing without adding
temporary C++ logging.
IFrame geometry changes and object representation changes directly
selected style invalidation reasons from their HTML element classes.
Move those mappings into a new
CSS::Invalidation::EmbeddedContentInvalidator.
The HTML elements continue to own their loading, representation, and
layout-tree side effects. CSS invalidation now owns the style dirtiness
associated with those embedded-content changes.
Element::set_shadow_root directly selected the style invalidation reason
used when a shadow root changes. Move that mapping into
CSS::Invalidation::ElementStateInvalidator.
Element still owns the shadow-root state transition and the required
layout-tree invalidation. CSS invalidation now owns the style dirtiness
for that state change.
Element::set_being_activated directly selected the style invalidation
reason used for :active changes. Move that mapping into
CSS::Invalidation::ElementStateInvalidator.
The element continues to own its activation state. CSS invalidation now
owns the style invalidation work associated with changing that state.
CustomStateSet directly selected the style invalidation reason used when
its JS-visible set is modified. Move that mapping into
CSS::Invalidation::CustomElementInvalidator.
This keeps the custom-state container focused on its set contents while
CSS invalidation owns the style work required by :state() selectors.
HTMLInputElement still mapped its picker open-state change directly to a
style invalidation reason. Move that mapping into
CSS::Invalidation::ElementStateInvalidator alongside the matching select
open-state helper.
This keeps another element-state invalidation decision out of the HTML
implementation without changing the invalidation behavior.
Several HTML element state changes directly selected style invalidation
reasons from their element implementations. Move those mappings into a
new CSS::Invalidation::ElementStateInvalidator helper.
This keeps details, dialog, option, and select code focused on their own
state changes while CSS invalidation owns the style work those changes
require. The existing invalidation breadth is preserved.
Document.cpp still flushed pending :has() invalidation by walking the
document and shadow-root style scopes directly. Move that CSS-specific
flush into CSS::Invalidation::HasMutationInvalidator.
Document continues to own the flag that says a :has() flush is needed.
The helper now owns the style-scope work needed to invalidate elements
affected by pending :has() mutations.
Document.cpp still handled CSS fallout from stylesheet media query match
changes directly. Move active stylesheet evaluation, rule-cache
invalidation, shadow-root fallout, and slot propagation into the CSS
invalidation helper.
Document continues to decide when media queries should be evaluated.
The helper now owns the style invalidation consequences when stylesheet
media queries change match state.
Document.cpp still knew how style changes on a slot propagate to
assigned light-DOM nodes. Move that flat-tree inheritance invalidation
into CSS::Invalidation::SlotInvalidator.
The style update walk continues to decide when an element's style
changed. The helper now owns the ::slotted() consequence of dirtying
assigned slottables for a changed slot.
AdoptedStyleSheets.cpp still handled CSS-side fallout from adopted sheet
list mutations directly. Move sheet attachment and media-query setup to
a CSS invalidation helper, along with rule-cache invalidation.
The observable array callbacks continue to validate JS values and
same-document construction rules. The helper now owns the CSS work for
when a constructed stylesheet becomes visible to, or is removed from, a
Document or ShadowRoot.
Slottable.cpp still handled the style invalidation fallout from slot
assignment changes directly. Move that ::slotted()-related policy into
CSS::Invalidation::SlotInvalidator.
Slot assignment remains DOM bookkeeping. The helper now owns the
choice to dirty element slottables when they gain or lose assignment
to a slot.
CharacterData.cpp still handled the style invalidation fallout from text
mutations under dir=auto ancestors directly. Move that behavior into the
CSS language invalidation helper.
CharacterData continues to own text replacement and layout text updates.
The helper now owns the inherited :dir() restyle and :has(:dir(...))
ancestor scheduling that can follow from text content changes.
Element.cpp still handled the style invalidation fallout from part and
exportparts attribute changes directly. Move that ::part-related policy
into CSS::Invalidation::PartInvalidator.
Element continues to update the DOM token state for part attributes. The
helper now owns the style dirtiness for elements targeted through ::part
and for shadow-tree descendants exposed through exportparts.