Commit graph

35 commits

Author SHA1 Message Date
Andreas Kling
8e473cb703 LibWeb: Bucket pseudo :is() rules by originating element
Split pseudo-element rules whose originating compound contains :is() or
:where() across the cheap buckets from their selector-list alternatives
when the rule would otherwise land in the pseudo-element other bucket.

Use per-collection generation stamps to suppress duplicate candidates
when more than one alternative bucket applies to the same originating
element. Add text coverage for class, attribute, and complex combinator
arguments reaching generated pseudo-element style.
2026-06-19 09:36:53 +02:00
Andreas Kling
f45bf0e770 LibWeb: Bucket ancestor-filtered rules by ancestor hash
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.
2026-06-19 09:36:53 +02:00
Andreas Kling
f39890bdc8 LibWeb: Bucket subject pseudo-class rules
Put rules whose rightmost compound has no id, class, tag, or
attribute bucket into a subject pseudo-class bucket when they have a
required cheap element-state pseudo-class such as :hover, :focus, or
:checked.

Also let :is() and :where() selector lists use a regular bucket when
every alternative exposes the same mandatory bucket key. Pseudo-class
invalidation rule caches keep using broad buckets because they must
consider rules while the matching state is changing.
2026-06-19 09:36:53 +02:00
Andreas Kling
b7a290cbe2 LibWeb: Keep pseudo-element rules in pseudo buckets
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.
2026-06-19 01:40:49 +02:00
Andreas Kling
849d528220 LibWeb: Build style invalidation data lazily
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.
2026-06-18 15:45:12 +02:00
Andreas Kling
66f313fe48 LibWeb: Preserve parsed user style sheets
Keep regular rule cache invalidation from discarding the parsed user
style sheet. User style and content blocker source changes now use an
explicit invalidation path that clears the parsed sheet before the rule
cache is rebuilt.

This avoids reparsing user CSS for unrelated style changes, which was
very annoying when browsing with cosmetic CSS from content blockers.
2026-06-18 13:27:39 +02:00
Sam Atkins
879fc69f89 LibWeb/CSS: Link DevTools rules to UA stylesheets
UA and user stylesheets do not have owner nodes or owner rules, so their
matched rules cannot be mapped through the generic stylesheet identifier
helper. Share the built-in UA stylesheet enumeration and map UA rules by
the matched CSSStyleSheet object, so that they stay in sync with any
future changes.
2026-06-04 20:54:33 +01:00
Sam Atkins
52e1d30404 LibWeb+LibDevTools: Report applied style rules to Firefox
Collect the style rules that apply to an inspected element and expose
them through the existing DOM node inspection path. This gives Firefox's
Rules panel real rule forms instead of the previous empty getApplied
response.
2026-06-04 20:54:33 +01:00
Sam Atkins
85e14f5f1f LibWeb: Apply scopes from imported stylesheets
Treat a scoped `@import` rule as a scope descriptor while traversing
style-producing rules from imported stylesheets. Imported rules now
inherit an outer scope for unscoped imports and replace it when the
import itself carries scope(...).

Scope resolution was generalized to handle both CSSScopeRule and
CSSImportRule descriptors. Import-scope boundaries are matched using
the stylesheet that parsed the `@import` rule, while normal imported
selectors keep using the imported stylesheet context. This lets
implicit scopes, nested `@scope` rules, :scope, and top-level `&` behave
as the Cascade 6 model requires.
2026-06-04 20:52:28 +01:00
Andreas Kling
6a05929c1c LibWeb: Reuse scoped rule matching scratch storage
Keep the temporary scoped matching rule list on StyleComputer.
This lets repeated style matching reuse its allocation instead of
creating a large stack object in collect_matching_rules().

Release builds zero that stack object before the constructor runs due
to automatic variable initialization. This was causing a 16 KiB memset
on every call to this function(!)

Verify that the scratch vector is empty on entry so accidental reentry
is caught before it can corrupt the outer rule collection.
2026-05-30 13:43:42 +02:00
Andreas Kling
817cb7432f LibWeb: Keep shadow rule caches for user styles
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.
2026-05-23 22:03:46 +02:00
Andreas Kling
44a6da5165 LibWeb: Avoid cold rule cache builds for :has() checks
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.
2026-05-23 22:03:46 +02:00
Tim Ledbetter
988dce3e7a LibWeb: Drop :local-link rule-cache gate from base URL change handler
The base URL change handler checked whether any style scope contained
a `:local-link` rule before walking the document's links, this forced
a rule cache rebuild, which is generally slower than the link walk we
were trying to avoid.

On Speedometer2 the duplicated rule-cache and invalidation-set
construction accounted for roughly 4% of total samples. Removing the
gate lets the URL-unchanged early-exit handle the no-op case and runs
the link walk only when the URL actually changes, which the profile
showed to be inexpensive on its own.
2026-05-23 19:43:42 +01:00
Sam Atkins
5c928eb7eb LibWeb/CSS: Implement the @scope rule
`@scope (a) to (b) {}` applies its contained style rules to elements
that have `a` as a parent, and do not have `a b` as a parent. Both the
`a` and `b` selector lists are optional.

Because it's situational whether a `@scope` will apply to a given
element, we store the ancestor scope on the `MatchingRule`, similar to
`@container`, and then determine during matching whether all the parent
`@scope`s match or not.

The rules for how selectors inside `@scope` are adjusted and interpreted
are a bit confusing. Unlike for other at-rules, nested style rules
inside `@scope` do not get a leading `&` added during parsing. To
support this, `adapt_nested_relative_selector_list()` now takes a flag
for whether its parent is a `@scope` or not.

`@scope` can also contain nested declarations without itself being
nested inside a style rule.

When determining their selectors, nested declarations rules adopt the
`@scope`'s scoping root if it has one, or otherwise fall back to the
parent element of the `<style>` element (not implemented here,) or the
`:root`. These are required to have zero specificity, so we wrap the
selector in `:where()`.
2026-05-22 10:00:42 +01:00
Sam Atkins
585849fcf0 LibWeb: Parse and evaluate @container <size-feature>s
e.g., `@container (width >= 300px) {}` and similar.

During style computation, flag any elements whose style depends on a
size container. Then re-evaluate their style after the initial layout
has been computed and size containers have a size. This may take
multiple passes, as these may have further descendants that depend on
their size, etc. We limit this to 8 passes currently.

SizeFeature itself is very similar to MediaFeature, but queries the
container element instead. There are only 6 size features specified, so
they're hard-coded instead of generated from JSON.

Also add a counter test for the narrower restyle path.
2026-05-20 13:00:50 +01:00
Sam Atkins
67046049d9 LibWeb: Evaluate container queries when collecting matching style rules
MatchingRules now have a container_rule member which stores the nearest
ancestor CSSContainerRule, if any. When populating the rule cache, we
maintain a stack of CSSContainerRules that we are within, and use
record the last one on the MatchingRule, so that it's O(1) instead of
having to walk up the rule's ancestors each time. This does mean we
have reimplement some "for each rule" code.

When collecting rules to apply to an element, we see if the MatchingRule
has a container_rule, and if so, we evaluate that rule's query to see
if the element has a matching container. We then also match any ancestor
container rules, using the cached parent container rule.

Add a custom test to cover the case of nested name-only `@container`s,
which WPT lacks currently as far as I can tell.
2026-05-13 11:05:31 +01:00
Tim Ledbetter
ac685a8a79 LibWeb: Only invalidate style on links matching :local-link
Currently, this is the only pseudo class which is URL-sensitive. When
we implement proper tracking of visited URLs this will need to be
replaced with something more comprehensive.
2026-05-08 12:30:29 +01:00
Andreas Kling
0b4cea5b29 LibWeb: Coalesce safe :has() child-list invalidations
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. :^)
2026-04-30 14:31:28 +02:00
Andreas Kling
59ae7d934a LibWeb: Skip unchanged :has() mutation fanout
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.
2026-04-30 00:24:04 +02:00
Andreas Kling
d5dd64cf97 LibWeb: Move :has() mutation invalidation out of StyleScope
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.
2026-04-30 00:24:04 +02:00
Tim Ledbetter
324ed5de0d LibWeb: Use an OrderedHashMap for pending :has() invalidations
Every DOM mutation that may affect a :has() selector enqueues an
entry in StyleScope keyed by an ancestor node. The entries were
previously stored  in a Vector and linearly scanned on every insert to
deduplicate by node. We now use an OrderedHashMap instead, eliminating
the quadratic deduplication.
2026-04-29 09:34:24 +02:00
Andreas Kling
ce5d0bdfc7 LibWeb: Narrow :has descendant invalidation fanout
When a :has() mutation is known to come from a specific subtree, use
that subtree as the mutation root while walking observed ancestors.

Before dirtying an anchor and its non-subject descendants, check whether
any cached :has() rule for that anchor can observe the changed subtree.
This keeps unrelated descendant mutations from invalidating every rule
that merely contains :has().
2026-04-28 15:34:49 +02:00
Andreas Kling
caad205467 LibWeb: Share singleton constructed stylesheet rule caches
Share the style cache for shadow roots whose only active author sheet is
the same constructed stylesheet. Matching already carries the effective
shadow root separately, so the cache can be reused while selectors such
as :host and ::slotted() still evaluate against each consuming shadow
root.

Keep the optimization conservative by falling back to the existing
per-scope cache whenever the shadow root has multiple active sheets, a
non-constructed sheet, or a page user stylesheet. Drop the shared cache
when the stylesheet rules or media query match state change.

Add coverage for two shadow roots adopting the same constructed sheet,
including :host, ::slotted(), and replaceSync() invalidation.
2026-04-28 13:07:52 +02:00
Andreas Kling
eed76b3619 LibWeb: Track rule scope outside MatchingRule
Keep cached MatchingRule entries independent from the shadow root that
owns the rule cache. Thread the effective rule shadow root through style
matching as transient state instead, so a rule cache can later be shared
by multiple scopes without copying every cached rule.

This preserves the existing matching behavior by deriving the effective
rule root from each cache lookup site. Pseudo-class invalidation already
operates on a single style scope, so it no longer needs a per-rule scope
filter.
2026-04-28 13:07:52 +02:00
Andreas Kling
029b4998e5 LibWeb: Skip useless sibling scans in generic :has() walks
Track whether any :has() relative selector in a style scope uses a
sibling combinator and let the generic ancestor walk consult that
before scanning ancestor siblings.

This keeps descendant-only :has() invalidations from walking unrelated
siblings while preserving the existing behavior for selectors that use
+ or ~. Add counter-based test coverage so the reduced sibling scans
stay visible through the invalidation counters.
2026-04-20 13:20:41 +02:00
Callum Law
64ccb9a015 LibWeb: Make @counter-style tree-scoped
This was a pretty straightforward change of storing registered counter
styles on the relevant `StyleScope`s and resolving by following the
process to dereference a global tree-scoped name, the only things of
note are:
 - We only define predefined counter styles (e.g. decimal) on the
   document's scope (since otherwise overrides in outer scopes would
   themselves be overriden).
 - When registering counter styles we don't have the full list of
   extendable styles so we defer fallback to "decimal" for undefined
   styles until `CounterStyle::from_counter_style_definition`.
2026-04-15 11:07:38 +01:00
Callum Law
8d117725e5 LibWeb: Add helper method to dereference global tree-scoped references 2026-04-15 11:07:38 +01:00
Andreas Kling
e2e3c7fcdf LibWeb: Rebuild counter style cache lazily
Stop rebuilding the counter style cache from every style update.
That made unrelated restyles pay the full counter-style cost even when
no relevant stylesheet state had changed.

Dirty the cache when stylesheet rule caches are invalidated and rebuild
it on the first counter-style lookup instead. Also make cold cache
rebuilds include user stylesheets.

Add regression tests covering insertRule() and replaceSync() updates
that should make newly defined counter styles take effect.
2026-04-05 12:34:28 +02:00
Andreas Kling
f1573dfc63 LibWeb: Use GC::WeakHashSet for StyleScope's pending :has() nodes
Replace the unsafe HashTable<GC::Weak<DOM::Node>> with
GC::WeakHashSet<DOM::Node>. The null check in the iteration loop is
no longer needed since WeakHashSet's iterator skips dead entries.
2026-02-24 22:35:03 +01:00
Callum Law
3d150e46c2 LibWeb: Avoid template for StyleScope::for_each_stylesheet
This only ever takes a `Function<void(CSS::CSSStyleSheet&)` so there is
no need to use a template here.
2026-02-23 11:21:09 +00:00
Callum Law
2cc3fbb017 LibWeb: Pass callback as lvalue ref in for_each_active_stylesheet
Taking the callback as an rvalue ref meant we couldn't use the same
callback more than once
2026-02-23 11:21:09 +00:00
Aliaksandr Kalenik
eea9837438 LibWeb: Skip :has() invalidation in update_style when nothing is pending
Add a document-level boolean flag that tracks whether any :has()
invalidations have been scheduled. This avoids iterating over all
shadow roots just to check is_empty() on each style scope when no
:has() invalidations are pending, which is the common case during
scrolling on complex pages like Reddit.

Results in ~10% reduction of is_empty() calls in profiles when
scrolling on Reddit.
2026-02-11 00:28:42 +01:00
Andreas Kling
a9cc425cde LibJS+LibWeb: Add missing GC marking visits
This adds visit_edges(Cell::Visitor&) methods to various helper structs
that contain GC pointers, and makes sure they are called from owning
GC-heap-allocated objects as needed.

These were found by our Clang plugin after expanding its capabilities.
The added rules will be enforced by CI going forward.
2026-01-07 12:48:58 +01:00
Sam Atkins
4ab9ac86a7 LibWeb/CSS: Implement basic ::part() matching
Matches elements with a `part` attribute, against their parent shadow
host.
2025-12-15 14:12:39 +00:00
Andreas Kling
66263f142b LibWeb: Add StyleScope to keep style caches per Document/ShadowRoot
Before this change, we've been maintaining various StyleComputer caches
at the document level.

This made sense for old-school documents without shadow trees, since
all the style information was document-wide anyway. However, documents
with many shadow trees ended up suffering since any time you mutated
a style sheet inside a shadow tree, *all* style caches for the entire
document would get invalidated.

This was particularly expensive on Reddit, which has tons of shadow
trees with their own style elements. Every time we'd create one of their
custom elements, we'd invalidate the document-level "rule cache" and
have to rebuild it, taking about ~60ms each time (ouch).

This commit introduces a new object called StyleScope.

Every Document and ShadowRoot has its own StyleScope. Rule caches etc
are moved from StyleComputer to StyleScope.

Rule cache invalidation now happens at StyleScope level. As an example,
rule cache rebuilds now take ~1ms on Reddit instead of ~60ms.

This is largely a mechanical change, moving things around, but there's
one key detail to be aware of: due to the :host selector, which works
across the shadow DOM boundary and reaches from inside a shadow tree out
into the light tree, there are various places where we have to check
both the shadow tree's StyleScope *and* the document-level StyleScope
in order to get all rules that may apply.
2025-11-14 22:05:33 +01:00