perf: speed up non-CSS-Modules CSS parsing by skipping unused AST work#21324
Conversation
Non-modules CSS parses never walk selectors, so consumeAQualifiedRule now scans the prelude without materializing its component-value nodes (url tokens and functions are kept so url() rewriting still fires). Trims ~4% off non-modules parsing; CSS-Modules parses are unchanged and output is byte-identical.
Comments now reach the visitor map via NodeType.Comment instead of a
separate process({ comment }) callback, giving one uniform interface for
every token/node. They fire during tokenization (source order among
comments) on a transient SoA node, so A.start/end/loc/source work like any
other node. No comment visitor registered means no callback and zero added
work. Behavior is unchanged; magic comments and CSS-Modules pure-mode still
read the same comments side-array.
Adds a per-parse skipLeafTypes set (buildSkipLeafSet helper) so component-value list builders can drop leaf node types nothing reads. Non-CSS-Modules parses now skip numeric / ident / hash / colon / delim value tokens (the Ident visitor no-ops and the Declaration visitor returns early there), which with the existing selector-prelude skip cuts non-modules parsing time by ~13%. CSS-Modules parses skip nothing — ICSS :export captures arbitrary value byte ranges. Output is byte-identical in both modes.
Collapses skipSelectorPrelude (boolean) and skipLeafTypes (Uint8Array) into a single CssProcessOptions.skip set indexed by NodeType: leaf-type entries drop value / function-arg leaves, the QualifiedRule entry drops selector preludes. buildSkipLeafSet becomes buildSkipSet. Behavior and output are unchanged.
Adds unit tests proving CssProcessOptions.skip drops each leaf type from both value and function-arg lists, that skipping a Function drops its whole subtree, that a combined set drops all listed types, that QualifiedRule drops selector preludes while keeping the block and url() rewrites, and that the object backend (parseA*) ignores a prior skip.
Records the two structural (not just consumer) constraints for reuse: skipping
both Delim and Ident loses !important detection, and skipping SimpleBlock loses
the custom-property {}-value check.
Completes the reusable skip mechanism: skip[AtRule] drops an at-rule prelude (symmetric with skip[QualifiedRule], keeping url tokens / functions so @import url(...) still resolves), and the stray-} value-list path now honours the skip set too, so every NodeType that can appear in a component-value list is skippable. Tests cover each leaf type, Function and SimpleBlock subtrees, both prelude toggles, and the object-backend reset. webpack still enables only its lean non-modules set; the added capability is for reuse.
The flat NodeType-indexed skip set was misleading: skip[Number] dropped number
nodes but skip[QualifiedRule]/skip[AtRule] dropped a rule's *prelude*, not the
rule. Replace it with a structured CssProcessOptions.skip:
{ types?: Uint8Array, selectorPrelude?: boolean, atRulePrelude?: boolean }
so value-type drops and prelude drops read unambiguously. buildSkipSet now
builds only the types set. Behavior and output unchanged.
The SourceProcessor constructor now takes default process options (skip, as, …) merged under each process() call's own options, so skip is configured on the processor instance and reused across parses rather than repeated per call. CssParser now sets as/skip on the processor and passes only the per-source locConverter to process(). Backward-compatible: HtmlParser and per-call options are unchanged.
🦋 Changeset detectedLatest commit: 0b31d42 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 |
|
This PR is packaged and the instant preview is available (00d8b2f). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@00d8b2f
yarn add -D webpack@https://pkg.pr.new/webpack@00d8b2f
pnpm add -D webpack@https://pkg.pr.new/webpack@00d8b2f |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #21324 +/- ##
==========================================
- Coverage 92.86% 92.43% -0.44%
==========================================
Files 594 594
Lines 65210 65269 +59
Branches 18143 18169 +26
==========================================
- Hits 60559 60333 -226
- Misses 4651 4936 +285
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:
|
There was a problem hiding this comment.
Pull request overview
This PR optimizes webpack’s internal CSS parser by allowing streaming parses to skip building/retaining AST parts that are not consumed for non-CSS-Modules styles, while keeping CSS-Modules behavior unchanged.
Changes:
- Added
CssProcessOptions.skip(includingbuildSkipSet) to selectively skip component-value nodes and optionally skip selector/at-rule preludes. - Routed comment handling through a
NodeType.Commentvisitor instead of a dedicatedcommentprocess callback. - Extended
util/SourceProcessorto accept default per-instance process options that are merged with per-call options.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| test/walkCssTokensParser.unittest.js | Updates comment expectations to NodeType.Comment and adds comprehensive unit coverage for skip behavior. |
| lib/util/SourceProcessor.js | Adds instance-level default options and merges them into per-call process() options. |
| lib/css/syntax.js | Implements skip state in the CSS streaming grammar, adds NodeType.Comment, and exports buildSkipSet. |
| lib/css/CssParser.js | Enables skipping only for non-CSS-Modules parses and switches comment collection to the comment visitor. |
| .changeset/css-skip-value-leaves.md | Changeset entry for value-token skipping perf improvement. |
| .changeset/css-skip-selector-prelude.md | Changeset entry for selector-prelude skipping perf improvement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
CI's tsc -p tsconfig.types.test.json inferred the tuple array as (string | number)[][], so indexing the counts record and calling buildSkipSet([type]) failed (TS7015 / TS2322). Annotate the array.
Merging this PR will improve performance by 85.5%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | benchmark "context-esm", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
566 KB | 947.1 KB | -40.24% |
| ❌ | Memory | benchmark "asset-modules-source", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
191.6 KB | 246.1 KB | -22.15% |
| ⚡ | Memory | benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
851.7 KB | 129.2 KB | ×6.6 |
| ⚡ | Memory | benchmark "asset-modules-bytes", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
858.9 KB | 247.8 KB | ×3.5 |
| ⚡ | Memory | benchmark "asset-modules-inline", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
806.4 KB | 381.6 KB | ×2.1 |
| ⚡ | Memory | benchmark "wasm-modules-async", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' |
343 KB | 189.2 KB | +81.29% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing claude/postcss-css-parser-perf-w7f7mt (0b31d42) with main (5461ae4)
Test the skip set for an explicit 1 rather than !== 0 so an out-of-range lookup on a short skip.types array keeps the node instead of dropping it.
Types CoverageCoverage after merging claude/postcss-css-parser-perf-w7f7mt into main will be
Coverage Report |
Summary
The CSS parser builds a full CSS-Syntax component-value tree even for parts nothing downstream reads. This adds a
CssProcessOptions.skipthat lets a parse leave unread parts un-materialized, on two independent axes:skip.types— aNodeType-indexed set of component-value node types to drop from declaration value / function-arg lists (built with the exportedbuildSkipSet);skip.selectorPrelude/skip.atRulePrelude— drop a qualified-rule / at-rule prelude (url tokens and functions are kept, sourl()in a selector and@import url(…)still resolve).CssParserenables this only for non-CSS-Modules parses, where theIdentvisitor no-ops, theDeclarationvisitor returns early, and there is no ICSS — so it drops selector preludes plus numeric/ident/hash/colon/delim value leaves. That trims non-modules parse time ~10–15% on large stylesheets (e.g. Tailwind) with byte-identical output. CSS-Modules parses skip nothing (selectors are walked and ICSS:exportcaptures arbitrary value ranges).Supporting refactors: comments now reach the visitor map through a new
NodeType.Commentinstead of an ad-hocprocess({ comment })callback (fired during tokenization on a transient node), andSourceProcessorcan carry default process options soskipis configured on the instance. Node materialization is unchanged for theparseA*entry points and the object backend. Refs the CSS parser performance work.What kind of change does this PR introduce?
perf (with supporting refactors: the unified
skipAPI, theNodeType.Commentvisitor, andSourceProcessorinstance options).Did you add tests for your changes?
Yes.
test/walkCssTokensParser.unittest.jsgains askip setsuite covering every skippable node type (each leaf type in both value and function-arg lists,Function/SimpleBlocksubtree drops,selectorPrelude/atRulePreludeincl. url-preservation, combined sets, the object-backend reset, and instance-levelskipoptions). The existingConfigTestCasesCSS cases and the CSS parsing/spec unit suites pass unchanged, and output was verified byte-identical (realwebpack()build diff) in both modes.Does this PR introduce a breaking change?
No. Output is byte-identical in both modes; the change is internal to the CSS parser.
If relevant, what needs to be documented once your changes are merged or what have you already documented?
n/a —
CssProcessOptions.skip,buildSkipSet, andNodeTypeare internal tolib/cssand not part of the public webpack API surface.Use of AI
Yes. This was developed with Claude (Anthropic) via Claude Code: profiling the parser hot paths, designing/implementing the
skipmechanism, writing the tests, and benchmarking against postcss / css-tree / cssom / Lightning CSS. All changes were human-reviewed; correctness is backed by the existing CSS test suite (config cases + parser unit/spec tests) and byte-identical build diffs. Per the webpack AI policy.🤖 Generated with Claude Code
Generated by Claude Code