Skip to content

perf: cut CSS/HTML parser memory and speed up long-token scanning - #21504

Merged
alexander-akait merged 8 commits into
mainfrom
perf/css-html-parser-memory-and-scan
Jul 25, 2026
Merged

perf: cut CSS/HTML parser memory and speed up long-token scanning#21504
alexander-akait merged 8 commits into
mainfrom
perf/css-html-parser-memory-and-scan

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Summary

The experimental CSS (lib/css/syntax.js) and HTML (lib/html/syntax.js) struct-of-arrays parsers carried two AST node columns that are always derivable from other stored fields, and several tokenizer states scanned long token bodies one JS char at a time while their sibling states already fast-forward with indexOf. This PR drops the redundant columns and gives the remaining char loops the same native-scan treatment — one idea applied to each parser (drop a derivable column; scan long runs natively).

Measured with standalone benchmarks against the parser modules (interleaved A/B, median of 23; before = the tree prior to these commits): CSS data:-URI-heavy −48.6%, HTML comment-heavy −81%, PI/bogus-comment −49%, large <plaintext> −29%, and every other workload neutral-or-faster (no regressions). Memory: retained CSS parseA* columns −11.8%, HTML peak columns −5.9% (one Int32 column each). Refs n/a.

What kind of change does this PR introduce?

perf

Did you add tests for your changes?

No new tests — every change is behavior-preserving, and the changed lines are exercised by the existing html5lib tree-construction conformance suite and the CssSyntax / HtmlSyntax unit tests (templates, comments, processing instructions, plaintext, strings, url() tokens, block rules). Each change was additionally validated by differential tree-serialization against the pre-change parser across the relevant 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 changes; no public API, config, or output surface changes.

Use of AI

Yes. Claude (via Claude Code) analyzed both SoA parsers to find divergences and optimization opportunities, measured every candidate for CPU and memory against the pre-change parser, and discarded the ones that did not hold up (an ident-sequence regex that regressed short idents, a widened dispatch table, intern-name lowercasing, parseA* pre-sizing, an iterative walk). The five changes here are the ones that measured as clear, regression-free wins, each verified behavior-preserving by differential testing. Analysis and implementation were AI-assisted as described.

🤖 Generated with Claude Code

https://claude.ai/code/session_016RA5wBcgNR7YdwtAnb3ysm


Generated by Claude Code

A `<template>` element and a raw-text element never coexist on one node, so
store a template's content-fragment ref in the existing `_nodeContentEnds`
slot gated by a new `FLAG_HAS_TEMPLATE` bit, dropping one Int32 AST node
column (~46 vs ~50 bytes/node of capacity). Behaviour-preserving and parse
time unchanged (measured neutral); ~6% less AST typed-array memory.
…column

A block rule's `blockEnd` always equals its `end` (the parser sets both to the
`}` position) and is `-1` when there is no block (`blockStart === -1`), so store
only `blockStart` and derive `blockEnd`. Drops the `_aux2` Int32 column across
all nodes: ~21% less retained memory for `parseA*` trees, and slightly faster
(one fewer column to grow/copy/snapshot). Behaviour-preserving.
`consumeAStringToken` / `consumeAUrlToken` read one JS char per code point, so a
long `data:` URI or base64 string costs thousands of reads. Add two module-scope
sticky regexes whose negated classes match exactly the loops' per-char
terminators, and fast-forward over the ordinary run via `test()` (advances
`lastIndex`, allocates nothing). ~45-65% faster on data-URI-heavy CSS, ~5% on
ordinary CSS; behaviour-preserving (token boundaries and url content ranges
unchanged).
The comment state read one JS char per code point while the data / RAWTEXT /
RCDATA / script-data states already fast-forward with a memoized `indexOf`.
Give the comment state the same treatment (a `nextHyphen` memo alongside
`nextLt` / `nextNul`), so long comments (license banners, commented-out blocks)
skip their body in native scans. ~80% faster on comment-heavy HTML, ~20% even
when the comment is hyphen-dense; behaviour-preserving.
The bogus-comment (`<?…>`, `<!…>`, `</bad>`) and plaintext states still read one
JS char per code point while every other long-scan tokenizer state fast-forwards
with `indexOf`. Give them the same treatment — a `nextGt` memo for bogus comments
and a single `indexOf` for the NUL-or-EOF plaintext body — so long processing
instructions and large `<plaintext>` bodies skip in native scans. ~40% / ~27%
faster on those inputs, neutral otherwise; behaviour-preserving.
Copilot AI review requested due to automatic review settings July 25, 2026 12:59
@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ba8064b

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 (fc5459f).

Install it locally:

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

@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.57%. Comparing base (61d4136) to head (ba8064b).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21504      +/-   ##
==========================================
- Coverage   93.57%   93.57%   -0.01%     
==========================================
  Files         619      619              
  Lines       73232    73242      +10     
  Branches    21105    21117      +12     
==========================================
+ Hits        68526    68534       +8     
- Misses       4706     4708       +2     
Flag Coverage Δ
css-parsing 25.60% <23.63%> (-0.01%) ⬇️
html5lib 27.07% <54.54%> (+<0.01%) ⬆️
integration 89.65% <74.54%> (-0.01%) ⬇️
test262 43.11% <9.09%> (-0.01%) ⬇️
unit 46.34% <100.00%> (+<0.01%) ⬆️

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.

Two lint errors on lines added by this branch: the CSS url fast-forward class
used `\x` control escapes (no-control-regex / unicorn/no-hex-escape /
escape-case), and the HTML content-end JSDoc put text on the `/**` line
(jsdoc/multiline-blocks). Switch the class to `\u` escapes with a justified
`no-control-regex` disable (the url token's terminators genuinely include the
control range and DEL), and reflow the JSDoc. No behavior change.
Copilot AI review requested due to automatic review settings July 25, 2026 13:07

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 12.14%

⚠️ 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
❌ 2 regressed benchmarks
✅ 213 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "css-modules", scenario '{"name":"mode-production","mode":"production"}' 6.9 MB 9.8 MB -28.77%
Memory benchmark "many-modules-commonjs", scenario '{"name":"mode-production","mode":"production"}' 7.6 MB 9.8 MB -22.39%
Memory benchmark "many-chunks-esm", scenario '{"name":"mode-production","mode":"production"}' 9.6 MB 7.8 MB +22.69%

Tip

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


Comparing perf/css-html-parser-memory-and-scan (ba8064b) with main (61d4136)

Open in CodSpeed

Restore the two pre-existing changesets (035 HTML, 050 CSS) that this branch had
folded into, and add a single dedicated changeset covering the two column drops
and the native token-body scans. Resolves the changeset-bot "no changeset" flag
and keeps this PR's release note separate from the earlier PRs' entries.
Copilot AI review requested due to automatic review settings July 25, 2026 13:11

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.

cspell doesn't know the plural noun "printables"; reword to "non-printable
code points", matching the file's existing `_isNonPrintableCodePoint` wording.
Copilot AI review requested due to automatic review settings July 25, 2026 13:16

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

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging perf/css-html-parser-memory-and-scan into main will be
99.34%
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.07%100%100%99.07%1048, 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,

@alexander-akait
alexander-akait merged commit fc5459f into main Jul 25, 2026
61 of 62 checks passed
@alexander-akait
alexander-akait deleted the perf/css-html-parser-memory-and-scan branch July 25, 2026 14:42
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