perf(css): single struct-of-arrays node store for the CSS parser, drop node classes - #21498
Merged
Conversation
🦋 Changeset detectedLatest commit: 79cfc6f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Contributor
|
This PR is packaged and the instant preview is available (61d4136). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@61d4136
yarn add -D webpack@https://pkg.pr.new/webpack@61d4136
pnpm add -D webpack@https://pkg.pr.new/webpack@61d4136 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21498 +/- ##
==========================================
+ Coverage 93.53% 93.57% +0.04%
==========================================
Files 619 619
Lines 73195 73219 +24
Branches 21098 21094 -4
==========================================
+ Hits 68460 68515 +55
+ Misses 4735 4704 -31
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The CSS syntax parser had two node backends behind mutable dispatch slots: a Struct-of-Arrays store for the streaming grammar (what CssParser/visitors actually use) and an object backend that built a Node/Token/Container class tree solely for the standalone parseA* entry points (used only by the unit tests and parser benchmark). Collapse to a single SoA backend. The parseA* entry points now parse into the SoA columns and hand back property-accessor readers over a retained snapshot, so the node classes and the whole object backend are gone. Removing the second backend also lets the dispatch slots become const aliases, so the consume algorithms call the SoA writers directly (no mutable-binding indirection). On the 1.9 MiB Tailwind fixture: parseAStylesheet ~115ms/46MiB -> ~91ms/21MiB (~21% faster, ~53% less retained heap); the streaming process path ~69 -> ~63ms.
alexander-akait
force-pushed
the
perf/css-syntax-soa-store
branch
from
July 25, 2026 07:46
aa92520 to
f4522c2
Compare
Build the reader prototype once at module scope instead of per store. Its getters read the reader's own `_store` snapshot, so all readers share one hidden map and stay monomorphic across parses. This removes the "wrong map" deopts on `_readerAt`/`_readRefList` and speeds up reader tree traversal by ~20%.
`_makeContainer` cleared the declaration / child-rule slots for every container. On a large non-recycling `parseA*` parse (e.g. a flat list of component values) those writes land at scattered high node ids and degrade the backing arrays to dictionary mode, making the parse ~4.5x slower. Only rules ever read these slots (the walk guards on type; readers normalize a missing slot to null), so only rules clear them now. This removes the dictionary-mode degradation — `parseAListOfComponentValues` on a big list drops from ~10.6ms to ~2.9ms (parity with the pre-SoA backend) — while keeping recycle-safety for the streaming build path.
The reader accessors `declarations` / `childRules` are typed on rule nodes; cast the function component value through the rule type so `tsconfig.types.test` type-checks the null-read assertion.
Contributor
Types CoverageCoverage after merging perf/css-syntax-soa-store into main will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The CSS syntax parser kept two node representations behind mutable dispatch slots: a struct-of-arrays store for the streaming
grammar(whatCssParserand the visitors actually use), and an object backend that built aNode/Token/Containerclass tree solely for the standaloneparseA*entry points (used only by the unit tests and the parser benchmark).This collapses it to a single struct-of-arrays backend. The
parseA*entry points now parse into the columns and hand back lazy property-accessor readers over a retained snapshot, so the node classes and the whole object backend are removed. With one backend the dispatch primitives becomeconst(no mutable-binding indirection), block-offset defaults are written only where a block is actually decided (at-rule / qualified-rule return paths) rather than on every container, and the internalsoa-prefixed identifiers are dropped now that there is nothing to disambiguate from.Measured on a 1.9 MiB stylesheet:
parseAStylesheet~112 → ~84 ms (~26% faster) and ~46 → ~21 MiB retained (~54% less); the streamingprocessparse+walk core ~67 → ~58 ms (~13% faster). Real-world full CSS builds (with and without CSS Modules) are neutral on wall time and identical on peak memory — they already ran the struct-of-arrays path and never built the classes — so this is a simplification plus a parser-core / standalone-API win at no build-path cost.What kind of change does this PR introduce?
perf
Did you add tests for your changes?
This is a behavior-preserving refactor, covered by the existing
test/CssSyntax.unittest.js(99 cases, which exercise the new reader accessors by readingnode.type/node.value/node.declarations/ etc.), thecssParsing-webpackspec suite, and the CSSConfigTestCasesintegration builds (761 cases) — all green, plus the full unit suite (28,084).test/CssSyntax.unittest.jswas updated to the class-free API: theNode/Tokenruntime exports are gone, so the twotoBeInstanceOf(Node|Token)assertions now checknode.type.Does this PR introduce a breaking change?
No.
lib/css/syntax.jsis an internal module —Node/Tokenwere never part of webpack's public API — so removing their runtime exports is not user-visible. The public config / API surface is unchanged.If relevant, what needs to be documented once your changes are merged or what have you already documented?
n/a
Use of AI
Yes — this change was developed with AI assistance (Claude Code). The refactor design, the code edits, the before/after CPU and memory benchmarking, and the test updates were carried out in an AI-assisted workflow, with each step reviewed and validated against the full test suite (unit, spec, and integration). Nothing was committed without verification.
Generated by Claude Code