Skip to content

perf: speed up non-modules CSS parsing - #21511

Merged
alexander-akait merged 6 commits into
mainfrom
claude/css-framework-profiling-7zkpqr
Jul 25, 2026
Merged

perf: speed up non-modules CSS parsing#21511
alexander-akait merged 6 commits into
mainfrom
claude/css-framework-profiling-7zkpqr

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Summary

Profiling the CSS parser against popular framework CSS (Tailwind, Bootstrap, Bulma, …) surfaced avoidable work on the real non-modules parse path (SKIP_NON_MODULES, the mode most builds hit). This lands four compounding, behavior-preserving optimizations in lib/css/syntax.js: (1) an advance() helper that skips the redundant next() re-check at guaranteed-non-EOF peek→advance sites; (2) in non-modules mode, dropping selector-prelude leaf tokens without materializing a node; (3) allocating a qualified rule's prelude list lazily so empty preludes cost nothing; (4) a fast path in _setValue that skips the flat-buffer seal for empty lists. Cumulatively ~22–27% faster on the non-modules parse path over the framework corpus (paired benchmark, sign test 60/60), with no change to the produced AST.

What kind of change does this PR introduce?

perf

Did you add tests for your changes?

No new tests — the changes are behavior-preserving and covered by the existing 157 CSS tests (all pass). Correctness was verified out-of-band with a differential AST fingerprint (byte-identical output in both the non-modules skip path and the full no-skip parseAStylesheet tree) over the framework corpus plus hand-picked edge cases.

Does this PR introduce a breaking change?

No.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

n/a — internal parser performance only; no public API or config change.

Use of AI

AI (Claude) was used to profile the CSS parser against a corpus of popular framework CSS, identify the hot paths, draft the optimizations, and build the differential-fingerprint and paired-benchmark harnesses used to validate byte-identical output and measure each change. All changes were reviewed and each was kept only after confirming identical AST fingerprints and a clear paired-benchmark win; candidate changes that regressed or altered output (e.g. skipping simple-block leaves) were discarded.


Generated by Claude Code

Profiling popular CSS frameworks (Bootstrap, Bulma, Foundation, Pico,
Tachyons, Tailwind) showed the token-stream plumbing (next/consume/discard)
among the hottest parse costs. The "peek, decide, advance" sites re-invoked
next() from consume()/discard() purely to re-derive the already-cached token.

Add TokenStream#advance(), a cursor bump that skips that redundant re-check,
and use it at the guaranteed-non-EOF sites: the whitespace-discard loops, the
declaration colon, block-contents whitespace/semicolon, skipped value leaves,
and the leaf component-value path (the bulk of nodes on a large stylesheet).

Output is byte-identical over 662k parsed nodes across the framework corpus
plus edge cases; a paired interleaved benchmark shows ~1-2% faster parsing.
In non-modules parsing the selector prelude is skipped (`selectorPrelude`),
yet consumeAQualifiedRule still built a store node for every prelude token —
class, id, combinator, pseudo — only to discard all but url()/function nodes.

Add a pre-materialization skip mirroring consumeAListOfComponentValues: leaf
selector tokens are advanced past without allocating a node, and the
`--foo: {` custom-property disambiguation reads the first two tokens' type and
start directly instead of from materialized nodes. Functions and `(`/`[`
blocks (which may hold a url) are still materialized.

Output is byte-identical (skip and no-skip fingerprints; all CSS tests pass);
the real non-modules path parses ~8-18% faster on selector-heavy frameworks.
In non-modules mode selector preludes are usually empty, so avoid taking a
scratch list from the pool and sealing it on every rule; allocate the prelude
list only on the first pushed node. `_makeContainer` already zeroes the rule's
list length, so an unsealed (null) prelude reads as empty in the walk.
In non-modules mode value/prelude leaf tokens are dropped, so most container
seals hand `_setValue` an empty list. `_makeContainer` already leaves the node's
list length at 0, so an empty seal can skip the flat-buffer bounds check, copy
loop, and start/length writes and just recycle the scratch array.
Copilot AI review requested due to automatic review settings July 25, 2026 19:31
@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c010239

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

This PR includes changesets to release 1 package
Name Type
webpack 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

This PR is packaged and the instant preview is available (7641790).

Install it locally:

  • npm
npm i -D webpack@https://pkg.pr.new/webpack@7641790
  • yarn
yarn add -D webpack@https://pkg.pr.new/webpack@7641790
  • pnpm
pnpm add -D webpack@https://pkg.pr.new/webpack@7641790

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.59%. Comparing base (61d4136) to head (c010239).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21511      +/-   ##
==========================================
+ Coverage   93.57%   93.59%   +0.02%     
==========================================
  Files         619      619              
  Lines       73232    73678     +446     
  Branches    21105    21258     +153     
==========================================
+ Hits        68526    68960     +434     
- Misses       4706     4718      +12     
Flag Coverage Δ
css-parsing 25.54% <75.60%> (-0.07%) ⬇️
html5lib 26.98% <9.75%> (-0.08%) ⬇️
integration 89.58% <92.68%> (-0.09%) ⬇️
test262 43.29% <9.75%> (+0.17%) ⬆️
unit 46.57% <100.00%> (+0.24%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI review requested due to automatic review settings July 25, 2026 19:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codspeed-hq

codspeed-hq Bot commented Jul 25, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 30.6%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 3 regressed benchmarks
✅ 212 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "devtool-eval", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 132.7 KB 342.1 KB -61.22%
Memory benchmark "context-esm", scenario '{"name":"mode-production","mode":"production"}' 7.6 MB 11.3 MB -32.97%
Memory benchmark "future-defaults", scenario '{"name":"mode-production","mode":"production"}' 6.9 MB 9.6 MB -27.78%
Memory benchmark "asset-modules-resource", scenario '{"name":"mode-development","mode":"development"}' 2.2 MB 1.8 MB +23.58%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/css-framework-profiling-7zkpqr (c010239) with main (d6afbe0)

Open in CodSpeed

@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging claude/css-framework-profiling-7zkpqr into main will be
99.32%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
bin
   webpack.js98.77%100%100%98.77%91
examples
   build-common.js100%100%100%100%
   buildAll.js100%100%100%100%
   examples.js100%100%100%100%
   template-common.js98.21%100%100%98.21%72
examples/custom-javascript-parser
   test.filter.js100%100%100%100%
examples/custom-javascript-parser/internals
   acorn-parse.js100%100%100%100%
   meriyah-parse.js100%100%100%100%
   oxc-parse.js100%100%100%100%
examples/markdown
   webpack.config.mjs100%100%100%100%
examples/module-federation
   test.filter.js100%100%100%100%
examples/reexport-components
   test.filter.js100%100%100%100%
examples/typescript
   test.filter.js100%100%100%100%
examples/typescript-non-erasable
   test.filter.js50%100%100%50%5
examples/virtual-modules
   test.filter.js100%100%100%100%
examples/wasm-bindgen-esm
   test.filter.js100%100%100%100%
examples/wasm-complex
   test.filter.js100%100%100%100%
examples/wasm-emscripten
   test.filter.js100%100%100%100%
examples/wasm-simple
   test.filter.js100%100%100%100%
examples/wasm-simple-source-phase
   test.filter.js100%100%100%100%
lib
   APIPlugin.js100%100%100%100%
   AsyncDependenciesBlock.js100%100%100%100%
   AutomaticPrefetchPlugin.js100%100%100%100%
   BannerPlugin.js100%100%100%100%
   Cache.js98.21%100%100%98.21%101
   CacheFacade.js100%100%100%100%
   Chunk.js99.72%100%100%99.72%39
   ChunkGraph.js100%100%100%100%
   ChunkGroup.js100%100%100%100%
   ChunkTemplate.js100%100%100%100%
   CircularModulesPlugin.js98.81%100%100%98.81%136
   CleanPlugin.js99.12%100%100%99.12%207, 227
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.42%100%100%98.42%1639, 1958, 1965, 1973, 1995, 1998, 2937, 3416–3417, 3449, 4149, 4179, 4232–4233, 4237, 4242, 4258–4259, 4273–4274, 4279–4280, 4757, 4783, 527, 532, 5591, 5623, 5640, 5656, 5672, 5687, 5712–5713, 5715, 6045, 6050, 6056, 6059, 6066, 6078, 6080, 6084, 6100, 6115, 6147, 6201, 6225, 6340, 778–779
   Compiler.js99.56%100%100%99.56%1147–1148, 1156
   ConcatenationScope.js98.65%100%100%98.65%195
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js99.88%100%100%99.88%1461
   ContextModuleFactory.js97.20%100%100%97.20%266, 435, 456, 461, 501, 512, 514, 518, 527–528
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99.08%100%100%99.08%1074, 176–177, 193, 212, 286
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.51%100%100%98.51%479, 525
   DependencyTemplate.js100%100%100%100%
   DependencyTemplates.js100%100%100%100%
   DotenvPlugin.js98.41%100%100%98.41%378, 391–392
   DynamicEntryPlugin.js100%100%100%100%
   EntryOptionPlugin.js100%100%100%100%
   EntryPlugin.js100%100%100%100%
   Entrypoint.js100%100%100%100%
   EnvironmentPlugin.js97.14%100%100%97.14%49
   ErrorHelpers.js100%100%100%100%
   EvalDevToolModulePlugin.js100%100%100%100%
   EvalSourceMapDevToolPlugin.js100%100%100%100%
   ExportsInfo.js100%100%100%100%
   ExportsInfoApiPlugin.js100%100%100%100%
   ExternalModule.js98.65%100%100%98.65%1196, 1199, 514–518, 520, 666
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.52%100%100%99.52%182, 2402–2403, 2406, 2417, 2428, 2439, 280, 3876, 3891, 3915
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.21%100%100%98.21%448, 457, 460, 464, 476
   FlagDependencyUsagePlugin.js100%100%100%100%
   FlagEntryExportAsUsedPlugin.js100%100%100%100%
   Generator.js100%100%100%100%
   HotModuleReplacementPlugin.js100%100%100%100%
   HotUpdateChunk.js100%100%100%100%
   IgnorePlugin.js100%100%100%100%
   IgnoreWarningsPlugin.js100%100%100%100%
   InitFragment.js100%100%100%100%
   JavascriptMetaInfoPlugin.js100%100%100%100%
   LazyBarrel.js100%100%100%100%
   LibraryTemplatePlugin.js100%100%100%100%
   LoaderOptionsPlugin.js100%100%100%100%
   LoaderTargetPlugin.js100%100%100%100%
   MainTemplate.js100%100%100%100%
   ManifestPlugin.js100%100%100%100%
   Module.js98.50%100%100%98.50%1288, 1293, 1353, 1367, 1429, 1438
   ModuleFactory.js100%100%100%100%
   ModuleFilenameHelpers.js98.85%100%100%98.85%106, 108
   ModuleGraph.js99.73%100%100%99.73%1005
   ModuleGraphConnection.js100%100%100%100%
   ModuleInfoHeaderPlugin.js100%100%100%100%
   ModuleNotFoundError.js100%100%100%100%
   ModuleProfile.js100%100%100%100%
   ModuleSourceTypeConstants.js100%100%100%100%
   ModuleTemplate.js100%100%100%100%
   ModuleTypeConstants.js100%100%100%100%
   MultiCompiler.js99.70%100%100%99.70%663
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.97%100%100%97.97%1008, 1025, 1273, 1307, 1323, 1770, 2067, 2072–2082, 34, 988, 991
   NormalModuleFactory.js98.72%100%100%98.72%1117, 1385, 1396, 1406, 1457–1459, 1466, 520, 532
   NormalModuleReplacementPlugin.js100%100%100%100%
   NullFactory.js100%100%100%100%
   OptimizationStages.js100%100%100%100%
   OptionsApply.js100%100%100%100%
   Parser.js100%100%100%100%
   PlatformPlugin.js100%100%100%100%
   PrefetchPlugin.js100%100%100%100%
   ProgressPlugin.js99.80%100%100%99.80%691
   ProvidePlugin.js100%100%100%100%
   RawModule.js100%100%100%100%
   RecordIdsPlugin.js100%100%100%100%
   RequestShortener.js100%100%100%100%
   ResolverFactory.js100%100%100%100%
   RuntimeGlobals.js100%100%100%100%
   RuntimeModule.js100%100%100%100%
   RuntimePlugin.js100%100%100%100%
   RuntimeTemplate.js100%100%100%100%
   SelfModuleFactory.js100%100%100%100%
   SingleEntryPlugin.js100%100%100%100%
   SourceMapDevToolModuleOptionsPlugin.js100%100%100%100%
   SourceMapDevToolPlugin.js98.62%100%100%98.62%220, 224, 226, 419, 430, 889
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.43%100%100%99.43%308–309
   UseStrictPlugin.js100%100%100%100%
   WarnCaseSensitiveModulesPlugin.js100%100%100%100%
   WarnDeprecatedOptionPlugin.js100%100%100%100%
   WarnNoModeSetPlugin.js100%100%100%100%
   WatchIgnorePlugin.js100%100%100%100%
   Watching.js100%100%100%100%
   WebpackError.js100%100%100%100%
   WebpackIsIncludedPlugin.js100%100%100%100%
   WebpackOptionsApply.js100%100%100%100%
   WebpackOptionsDefaulter.js100%100%100%100%
   buildChunkGraph.js99.87%100%100%99.87%371
   cli.js98.63%100%100%98.63%10, 119, 549, 581, 631, 905
   index.js99.72%100%100%99.72%184
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js97.10%100%100%97.10%10, 263, 285, 287
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.95%100%100%97.95%295, 319, 322, 42, 452, 47
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
   WebManifestGenerator.js100%100%100%100%
   WebManifestParser.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
   isGeneratorLowered.js100%100%100%100%
lib/bun
   BunTargetPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%75, 87, 95
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%107, 114–115, 123, 90
   PackFileCacheStrategy.js96.41%100%100%96.41%1257, 1357, 1361, 1423, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 723, 729, 763, 769, 775, 780, 791, 800, 805–806, 808, 825, 831–832, 834
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.37%100%100%99.37%1620–1622, 1630,

Copy link
Copy Markdown
Member Author

The CodSpeed future-defaults memory regression is a false positive, not something this PR can cause:

  • This PR changes only lib/css/syntax.js (the CSS parser). The future-defaults benchmark is a pure-JS build — its entry imports a generated JS module and its config only sets experiments.futureDefaults, with no CSS anywhere — so the changed code never runs in that benchmark.
  • CodSpeed itself flags "different runtime environments detected … may affect the accuracy of the results," and an earlier run of the same report showed the opposite (a memory improvement with zero regressions). The two flagged benchmarks swing ~25% on small absolute values (6.9→9.6 MB, 2.2→1.8 MB), which is the signature of cross-environment measurement noise.

The changes here strictly reduce CSS-parser allocations (lazy prelude, empty-list seal fast path) and are behaviour-preserving (byte-identical AST verified via differential fingerprint), so a real memory increase — especially in a build that parses no CSS — isn't possible. Nothing to fix in code.


Generated by Claude Code

@alexander-akait
alexander-akait merged commit 7641790 into main Jul 25, 2026
68 of 70 checks passed
@alexander-akait
alexander-akait deleted the claude/css-framework-profiling-7zkpqr branch July 25, 2026 22:23
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.

2 participants

Sponsor
SponsoredKunjungi sekarang
Promo