Skip to content

Append inserted rules to the buckets instead of rebuilding - #3470

Merged
karlseguin merged 2 commits into
mainfrom
incremental-insert-rule
Sep 11, 2026
Merged

karlseguin merged 2 commits into
mainfrom
incremental-insert-rule

Conversation

@arrufat

@arrufat arrufat commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Based on #3467 (independent of #3468 / #3469).

CSSStyleSheet.insertRule called sheetModified, so the next visibility query re-parsed every stylesheet in the document: every selector re-tokenized, every @media query re-evaluated, every layer re-ranked. CSS-in-JS libraries insert rules one at a time, and a component that reads layout right after mounting pays a full rebuild per rule.

StyleManager.ruleInserted now appends directly when the rule is a style rule appended at or after the sheet that emitted the last visibility rule (last_rule_sheet, tracked by the rebuild and by appends). That is the invariant the fast path needs, "no rule with a higher document order exists", rather than list position, so a trailing <style> holding only keyframes or @font-face does not force rebuilds on the sheet before it. The appended rule packs the next document order and, being unlayered, UNLAYERED_RANK; unlayered rules now carry that rank from creation on the rebuild path too, so an append is byte-for-byte what a rebuild would produce and stampRuleList only touches layered rules. An append that sets none of display / visibility / opacity / pointer-events (most CSS-in-JS rules) changes nothing and does not even stamp the memo. Everything else still rebuilds: an insert in the middle, an insert into an earlier sheet, an at-rule, or an insert while a rebuild is already pending.

Two things this cleaned up on the way:

  • addRawRule (the <style> text path) folded declarations with a plain overwrite, so .x { display: none !important; display: block } read as block until cssRules was touched and the CSSOM path took over. It now shares the !important-aware fold the inline scan already used, and materializing a <style> sheet's cssRules no longer counts as a sheet change since both paths agree.
  • One addVisibilityRule replaces the three copies of the per-selector loop; bucket uses getOrPutValue; Selector.rightmost() is shared with the selector matcher.

Tests: a unit test asserts the append path leaves dirty clear and agrees with the rebuild path on ordering, and that middle inserts and at-rules rebuild; fixture scripts check specificity ordering across appends, appends to an earlier sheet, an unlayered append outranking a layered rule, and the !important text case surviving cssRules materialization.

Numbers (ReleaseFast, static theverge fixture): a loop of 200 × insertRule each followed by checkVisibility() goes from 3.0 ms to 0.5 ms, median of 3.

@arrufat
arrufat added this pull request to stack #3471 September 9, 2026 19:58
@arrufat
arrufat force-pushed the incremental-insert-rule branch from 173168d to 63d67a2 Compare September 9, 2026 20:21
CSSStyleSheet.insertRule marked every sheet dirty, so the next visibility
query re-parsed every stylesheet. A style rule appended at or after the
sheet that emitted the last rule is last in cascade order: it now joins
the buckets with the next document order and only stamps the memo, and
an append that sets none of the tracked properties changes nothing at
all. Inserts in the middle, into an earlier sheet, or of at-rules still
take the rebuild path.

Unlayered rules carry UNLAYERED_RANK from creation, so an appended rule
packs exactly what a rebuild would. Materializing a <style> sheet's
cssRules no longer counts as a change, since the text and rule paths
now fold declarations the same way (!important included).
@arrufat
arrufat force-pushed the incremental-insert-rule branch from 63d67a2 to 90bf686 Compare September 9, 2026 20:49
Base automatically changed from style-visibility-memo to main September 10, 2026 03:02
@karlseguin

Copy link
Copy Markdown
Collaborator

It's a good chance, but it needs to be rebased, and, in particular, it conflicts with the --* custom property support. insertRule(":root { --brand: red }") would get ignored now.

main added custom property (`--*`) tracking to the same rule-building code
this branch made appendable. Both paths now share addSelectorRules, which
registers the custom declarations of a rule and its visibility rules, and
reports whether it added anything tracked. An insertRule append therefore
reaches custom_rules too, and clears the lazily parsed selectors of a
property it joins, instead of being dropped when the rule sets no
visibility property.

The raw-text path folds a block once for both kinds of declaration, so a
rebuild does not tokenize every block twice. rule_layers lives in the rule
arena so an appended rule can register its layer; the layered ranks are
still stamped by finalizeLayerRanks.
@arrufat

arrufat commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Merged main in. You were right: the fast path only looked at visibility properties, so a rule with only --* declarations was treated as "nothing changed" and never registered.

Both paths now go through the same addSelectorRules, which registers a rule's custom declarations and its visibility rules, and returns whether it added anything. An appended :root { --brand: red } reaches custom_rules like it does on a rebuild. If that property was already looked up, its lazily parsed selectors are cleared so the next lookup sees the new rule.

Added a case to custom_properties.html that reads --brand, appends the :root rule to the last sheet, checks the value changed, then deletes it and checks it went back.

@karlseguin
karlseguin merged commit 28c1a69 into main Sep 11, 2026
26 checks passed
@karlseguin
karlseguin deleted the incremental-insert-rule branch September 11, 2026 09:09
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants