Skip to content

Skip redundant CSS variable re-flattening when nothing new was declared - #2169

Open
lukasbob wants to merge 1 commit into
mainfrom
fix/skip-variable-reflatten
Open

lukasbob wants to merge 1 commit into
mainfrom
fix/skip-variable-reflatten

Conversation

@lukasbob

@lukasbob lukasbob commented Sep 2, 2026

Copy link
Copy Markdown

Summary

Style.of() merges the parent's already-flattened custom-property (variable) map with the current element's own declared variables, then re-flattens the whole merged set — unconditionally, even when the element declares no variables of its own (the common case), in which case the merge is a no-op and re-flattening just reproduces the parent's map at full cost.

Profiled sia-r65 (~140s on a 10k-node real-world page) with Node's CPU profiler: 39.5% of all execution time was inside Hash.writeString, traced through Map.set → Variable.flatten → Style.of → Style.from. The fixture defines 1,742 distinct custom properties (a design-token system); every style computation was re-substituting var() references across however many of those were in scope, from scratch, regardless of whether anything could have changed.

This isn't rule- or context-specific — it taxes every rule on any page with a nontrivial custom-property set. sia-r65 just makes it visible because its per-target scan produces far more uncached style computations than most rules ever do.

Fix

Skip Variable.flatten() and reuse the parent's variables map directly when the current element's own Variable.gather() result is empty — safe because parent.variables is already flattened by construction (every Style instance is built through this same function), so re-flattening it with nothing new merged in is a no-op.

Measured impact

Verified identical pass/fail/cantTell/inapplicable outcomes across every rule in the rule set, on both a small and a 10k-node fixture, with and without the fix — this is a caching fix, not a behavior change.

On the 10k-node fixture, a single fresh evaluation (not a repeat-evaluation benchmark artifact — confirmed via isolated single-pass timing, unlike the earlier Context-interning PR #2168):

Rule Before After Change
sia-r65 129.5s 39.6s -69.4%
sia-r62 218ms 133ms -39.0%

Full rule-set run (all ~90 rules) on the same fixture: total duration dropped from ~143s to ~44s.

Test plan

  • yarn build packages/alfa-style (+ downstream: alfa-rules, alfa-act, alfa-cascade, alfa-aria)
  • yarn test packages/alfa-style packages/alfa-rules packages/alfa-act packages/alfa-cascade packages/alfa-aria — 2302/2302 passing
  • Outcome-count diff across every rule in the rule set, both fixtures, with/without the fix — zero differences
  • CPU-profiled before/after to confirm the fix actually eliminates the identified hotspot
  • yarn changeset status — clean

🤖 Generated with Claude Code

Style.of() merged the parent's already-flattened custom-property
(variable) map with the current element's own declared variables, then
re-flattened the whole merged set unconditionally - even when the
element declared no variables of its own, in which case the merge is
a no-op and re-flattening reproduces the parent's map exactly.

Profiled sia-r65 (~140s on a 10k-node real-world page) with Node's CPU
profiler: 39.5% of all execution time was inside Hash.writeString,
traced through Map.set -> Variable.flatten -> Style.of -> Style.from.
The page defines 1,742 distinct custom properties (a design-token
system); every style computation re-substituted var() references
across however many of those were in scope, from scratch, regardless
of whether anything could have changed.

This isn't rule- or context-specific - it taxes every rule on any page
with a nontrivial custom-property set. sia-r65 just makes it visible
because its per-target scan produces far more uncached style
computations than most rules ever do.

Verified identical pass/fail/cantTell/inapplicable outcomes across
every rule in the rule set, on both a small and a 10k-node fixture,
with and without the fix. On the 10k-node fixture, a single fresh
evaluation (not a repeat-evaluation benchmark artifact, unlike the
Context-interning fix): sia-r65 129.5s -> 39.6s (-69.4%), sia-r62
218ms -> 133ms (-39.0%).
@lukasbob
lukasbob requested a review from a team as a code owner September 2, 2026 22:16
@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e63589

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 76 packages
Name Type
@siteimprove/alfa-style Patch
@siteimprove/alfa-act Patch
@siteimprove/alfa-affine Patch
@siteimprove/alfa-applicative Patch
@siteimprove/alfa-aria Patch
@siteimprove/alfa-array Patch
@siteimprove/alfa-bits Patch
@siteimprove/alfa-branched Patch
@siteimprove/alfa-cache Patch
@siteimprove/alfa-callback Patch
@siteimprove/alfa-cascade Patch
@siteimprove/alfa-clone Patch
@siteimprove/alfa-collection Patch
@siteimprove/alfa-comparable Patch
@siteimprove/alfa-continuation Patch
@siteimprove/alfa-css-feature Patch
@siteimprove/alfa-css Patch
@siteimprove/alfa-device Patch
@siteimprove/alfa-dom Patch
@siteimprove/alfa-eaa Patch
@siteimprove/alfa-earl Patch
@siteimprove/alfa-either Patch
@siteimprove/alfa-emitter Patch
@siteimprove/alfa-encoding Patch
@siteimprove/alfa-equatable Patch
@siteimprove/alfa-flags Patch
@siteimprove/alfa-fnv Patch
@siteimprove/alfa-foldable Patch
@siteimprove/alfa-functor Patch
@siteimprove/alfa-generator Patch
@siteimprove/alfa-graph Patch
@siteimprove/alfa-hash Patch
@siteimprove/alfa-http Patch
@siteimprove/alfa-iana Patch
@siteimprove/alfa-iterable Patch
@siteimprove/alfa-json-ld Patch
@siteimprove/alfa-json Patch
@siteimprove/alfa-lazy Patch
@siteimprove/alfa-list Patch
@siteimprove/alfa-map Patch
@siteimprove/alfa-mapper Patch
@siteimprove/alfa-math Patch
@siteimprove/alfa-monad Patch
@siteimprove/alfa-network Patch
@siteimprove/alfa-option Patch
@siteimprove/alfa-painting-order Patch
@siteimprove/alfa-parser Patch
@siteimprove/alfa-performance Patch
@siteimprove/alfa-predicate Patch
@siteimprove/alfa-record Patch
@siteimprove/alfa-rectangle Patch
@siteimprove/alfa-reducer Patch
@siteimprove/alfa-refinement Patch
@siteimprove/alfa-result Patch
@siteimprove/alfa-rng Patch
@siteimprove/alfa-rules Patch
@siteimprove/alfa-sarif Patch
@siteimprove/alfa-selective Patch
@siteimprove/alfa-selector Patch
@siteimprove/alfa-sequence Patch
@siteimprove/alfa-set Patch
@siteimprove/alfa-slice Patch
@siteimprove/alfa-string Patch
@siteimprove/alfa-table Patch
@siteimprove/alfa-test Patch
@siteimprove/alfa-thunk Patch
@siteimprove/alfa-time Patch
@siteimprove/alfa-toolchain Patch
@siteimprove/alfa-trampoline Patch
@siteimprove/alfa-tree Patch
@siteimprove/alfa-trilean Patch
@siteimprove/alfa-tuple Patch
@siteimprove/alfa-url Patch
@siteimprove/alfa-wcag Patch
@siteimprove/alfa-web Patch
@siteimprove/alfa-xpath Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant