Skip to content

perf: speed up CSS and HTML parsing and code generation#21181

Merged
alexander-akait merged 8 commits into
mainfrom
perf/css-html-parser-codegen
Jun 13, 2026
Merged

perf: speed up CSS and HTML parsing and code generation#21181
alexander-akait merged 8 commits into
mainfrom
perf/css-html-parser-codegen

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary

Constant-factor performance work across the CSS and HTML parsers, their code generators, and the related dependency templates — no behavior change. Highlights: memoize the CSS parser's per-module getKnownProperties setup and dedupe parseResource; drop per-token/per-export replace/toLowerCase/Map work (reuse the already-normalized declaration name, short-circuit empty ICSS lookups); hoist per-call regexes/closures in the CSS/HTML dependency templates; move HTML sibling-tag native-tag detection and CSP attribute capture to the parse step instead of re-parsing tag text with regex at code-gen; and a profiler-guided url() fast path (lazy webpackIgnore warning loc + guarded normalizeUrl replaces). Profiling-driven: a parse-isolated benchmark shows ~20% faster CSS parse on a url()-heavy stylesheet, and emitted output is byte-identical.

What kind of change does this PR introduce?

perf

Did you add tests for your changes?

Yes — test/configCases/html/script-src-sibling-attrs covers the new parse-time CSP-attribute capture (quoted/bare/unquoted source forms copied byte-exact onto a synthesized sibling <link>). Otherwise these are behavior-preserving optimizations already covered by the existing CSS/HTML ConfigTestCases/ConfigCacheTestCases, the cssParsing/html5lib spec suites, and the HtmlParser/CssIcssExportDependency unit tests, all passing with unchanged snapshots.

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

Use of AI

Yes — I used Claude (Anthropic) to profile the parsers, identify the hot paths, implement these optimizations, and validate them against the full CSS/HTML test suites and a parse-isolated benchmark. All changes were reviewed for correctness and behavior-identical output.

- CssParser: cache the normalized structural name so localGlobalActive /
  icssActive / composesAnchorSkip stop re-running a regex replace +
  toLowerCase per value token; add a stripBackslashes fast path; hoist the
  getComments comparator and skip its work when there are no comments;
  share the empty localIdentifiers/composesFiles containers in non-modules
  mode instead of copying them per rule.
- css/syntax: test the source bytes for the '--' custom-property prefix
  instead of forcing the lazy token value slice.
- CssGenerator: prefix-test the module type in getTypes instead of
  allocating a split() array per incoming connection.
- HtmlGenerator: hoist the chunk-URL sentinel regex to module scope.
- HtmlModulesPlugin: hoist createHash/nonNumericOnlyHash requires.
- HtmlParser: reuse syntax.js findAttr instead of allocating an
  Array#find closure per script element.
- CssIcssExportDependency.getLocalIdent: hoist the 4 ident-sanitizing
  regexes + the prepareId closure to module scope; drop the redundant
  /[local]/.test guard before the replace.
- CssUrlDependency.cssEscapeString: hoist the 3 escape regexes + shared
  replacer.
- HtmlScriptSrcDependency: precompile the copyable-attribute matchers and
  the native-tag check instead of building RegExp from strings per call;
  gate the integrity strip behind includes(); precompute the CSS chunk
  tie-break sort key once per chunk; hoist the CSS source-type pair.
- HtmlInlineStyleDependency: fold the 3 sequential attribute-escape passes
  into one regexp pass (byte-identical output).
- CssModule.identifier: build the inheritance segment with a loop instead
  of map().join() (byte-identical).
HtmlScriptSrcDependency's template re-parsed the cloned tag's source text
with a per-apply RegExp (isNativeTagForKind) to tell a native <script>/<link>
from a custom element mapped to a source type. The parser already knows the
tag name, so compute the native flag there and thread it through the
dependency (tagIsNative); the template reads the boolean instead of
re-parsing. Byte-identical output (verified against the html config + cache
snapshots).
buildStylesheetLink / buildScriptTag re-parsed the originating tag's
nonce/crossorigin/referrerpolicy out of the source text with three RegExps
each time a sibling <link>/<script> was synthesized. The parser already has
those attributes with byte offsets, so capture their exact source spans there
(copyableAttrsText) and pass them through the dependency; the template just
concatenates the precomputed text. Removes the COPYABLE_ATTR_REGEXPS scan
from code-gen. Byte-identical (verified against the html config + cache
snapshots, incl. css-imported-from-js which copies all three attrs).
…work

- CssParser: memoize getKnownProperties on the parser instance (it depends
  only on the fixed option flags and the instance is reused across modules),
  instead of rebuilding the Map + ~10 record spreads per module.
- CssParser: resolve the module resource once (parseResource is uncached
  without a cache object) and reuse it for the auto-mode check + self-reference
  resolution.
- CssParser: getReexport short-circuits when there are no ICSS/@value
  definitions, skipping the `--`-key concat + Map probe per export.
- CssParser: the url-in-declaration skip check reads the already-normalized
  currentStructuralName instead of re-running the vendor-prefix regex +
  toLowerCase per url() token.
- HtmlParser: hoist the allowed <link rel> list to a module-level Set so
  filterLinkHref does set lookups instead of allocating a 10-element array +
  .some closure per <link>.
All byte-identical (css + html config snapshots unchanged).
… fast paths

Profiling a url()-heavy stylesheet showed emitUrlFunction dominating, via
eager loc computation and unconditional regex replaces:
- webpackIgnored computed the invalid-webpackIgnore warning loc eagerly for
  every checked url() / @import, though it's only used in that rare warning.
  Pass the warn range and compute rangeLoc lazily inside the branch.
- normalizeUrl ran STRING_MULTILINE / TRIM_WHITE_SPACES / UNESCAPE replaces
  unconditionally; gate each on a cheap charCode/includes check so the common
  escape-free, edge-whitespace-free URL skips the regex engine.
~6% faster on a url-in-every-rule fixture; the url path no longer shows in the
profile. Byte-identical (css config + spec snapshots unchanged).
@changeset-bot

changeset-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 48b56c5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.65625% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.68%. Comparing base (0bf661b) to head (48b56c5).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/dependencies/HtmlScriptSrcDependency.js 84.21% 3 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #21181   +/-   ##
=======================================
  Coverage   92.68%   92.68%           
=======================================
  Files         587      587           
  Lines       63962    64005   +43     
  Branches    17726    17748   +22     
=======================================
+ Hits        59280    59322   +42     
- Misses       4682     4683    +1     
Flag Coverage Δ
css-parsing 28.71% <40.54%> (+0.02%) ⬆️
html5lib 31.22% <22.65%> (+0.01%) ⬆️
integration 88.64% <97.65%> (+<0.01%) ⬆️
test262 45.49% <16.21%> (+<0.01%) ⬆️
unit 41.05% <31.74%> (+0.02%) ⬆️

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.

@codspeed-hq

codspeed-hq Bot commented Jun 13, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 44.75%

⚠️ 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
✅ 140 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "lodash", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 126.1 KB 858.1 KB -85.31%
Memory benchmark "side-effects-reexport", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 761.8 KB 1,188.9 KB -35.92%
Memory benchmark "many-chunks-esm", scenario '{"name":"mode-production","mode":"production"}' 7.2 MB 9.5 MB -23.49%
Memory benchmark "many-modules-esm", scenario '{"name":"mode-production","mode":"production"}' 9.6 MB 7.4 MB +29.4%

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-codegen (48b56c5) with main (0bf661b)

Open in CodSpeed

@github-actions

Copy link
Copy Markdown
Contributor

Types Coverage

Coverage after merging perf/css-html-parser-codegen 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.js91.30%100%100%91.30%140, 142–143, 145, 147, 153–154, 161, 168, 90
examples/markdown
   webpack.config.mjs100%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-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%
   CleanPlugin.js99.15%100%100%99.15%206, 226
   CodeGenerationResults.js100%100%100%100%
   CompatibilityPlugin.js100%100%100%100%
   Compilation.js98.44%100%100%98.44%1609, 1905, 1912, 1920, 1942, 2838, 3317–3318, 3350, 4014, 4044, 4097–4098, 4102, 4107, 4123–4124, 4138–4139, 4144–4145, 4622, 4648, 513, 518, 5456, 5488, 5505, 5521, 5537, 5552, 5577–5578, 5580, 5908, 5913, 5919, 5922, 5934, 5936, 5940, 5956, 5971, 6003, 6057, 6081, 6195, 763–764
   Compiler.js99.56%100%100%99.56%1135–1136, 1144
   ConcatenationScope.js98.59%100%100%98.59%189
   ConditionalInitFragment.js100%100%100%100%
   ConstPlugin.js100%100%100%100%
   ContextExclusionPlugin.js100%100%100%100%
   ContextModule.js100%100%100%100%
   ContextModuleFactory.js97.40%100%100%97.40%258, 395, 418, 420, 424, 433–434
   ContextReplacementPlugin.js100%100%100%100%
   DefinePlugin.js99%100%100%99%171–172, 188, 207, 281
   DependenciesBlock.js100%100%100%100%
   Dependency.js98.15%100%100%98.15%379, 425
   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.97%100%100%98.97%425–429, 577
   ExternalModuleFactoryPlugin.js100%100%100%100%
   ExternalsPlugin.js100%100%100%100%
   FileSystemInfo.js99.50%100%100%99.50%182, 2252–2253, 2256, 2267, 2278, 2289, 278, 3693, 3708, 3732
   FlagAllModulesAsUsedPlugin.js100%100%100%100%
   FlagDependencyExportsPlugin.js98.46%100%100%98.46%425, 434, 436, 440
   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%
   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%1285, 1290, 1350, 1364, 1426, 1435
   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.69%100%100%99.69%659
   MultiStats.js100%100%100%100%
   MultiWatching.js100%100%100%100%
   NoEmitOnErrorsPlugin.js100%100%100%100%
   NodeStuffPlugin.js100%100%100%100%
   NormalModule.js97.90%100%100%97.90%1240, 1243, 1260, 1277, 1524, 1558, 1574, 1661, 2017, 2316, 2321–2331, 419, 423, 577
   NormalModuleFactory.js99.47%100%100%99.47%1083, 1392, 486, 498
   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.js98.85%100%100%98.85%519–520, 525, 527, 591
   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, 891
   Stats.js100%100%100%100%
   Template.js100%100%100%100%
   TemplatedPathPlugin.js99.17%100%100%99.17%176–177
   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.62%100%100%98.62%10, 119, 545, 577, 627, 897
   index.js99.72%100%100%99.72%179
   validateSchema.js94.67%100%100%94.67%100, 87, 89, 98
   webpack.js96.33%100%100%96.33%10, 198, 220, 222
lib/asset
   AssetBytesGenerator.js100%100%100%100%
   AssetBytesParser.js100%100%100%100%
   AssetGenerator.js100%100%100%100%
   AssetModule.js100%100%100%100%
   AssetModulesPlugin.js97.32%100%100%97.32%281, 305, 308, 36, 360, 41
   AssetParser.js100%100%100%100%
   AssetSourceGenerator.js100%100%100%100%
   AssetSourceParser.js100%100%100%100%
   RawDataUrlModule.js100%100%100%100%
lib/async-modules
   AsyncModuleHelpers.js100%100%100%100%
   AwaitDependenciesInitFragment.js100%100%100%100%
   InferAsyncModulesPlugin.js100%100%100%100%
lib/cache
   AddBuildDependenciesPlugin.js100%100%100%100%
   AddManagedPathsPlugin.js100%100%100%100%
   IdleFileCachePlugin.js97.92%100%100%97.92%71, 83, 91
   MemoryCachePlugin.js95.83%100%100%95.83%33
   MemoryWithGcCachePlugin.js93.15%100%100%93.15%106, 113–114, 122, 89
   PackFileCacheStrategy.js96.40%100%100%96.40%1250, 1350, 1354, 1416, 628, 647, 657–659, 661, 677–678, 683, 686, 688, 693, 698, 722, 728, 762, 768, 774, 779, 790, 799, 804–805, 807, 824, 830–831, 833
   ResolverCachePlugin.js100%100%100%100%
   getLazyHashedEtag.js100%100%100%100%
   mergeEtags.js100%100%100%100%
lib/config
   browserslistTargetHandler.js100%100%100%100%
   defaults.js99.30%100%100%99.30%1439–1441, 1449, 274, 277, 282, 286
   normalization.js99.01%100%100%99.01%191–192, 258, 273
   target.js100%100%100%100%
lib/container
   ContainerEntryDependency.js100%100%100%100%
   ContainerEntryModule.js100%100%100%100%
   ContainerEntryModuleFactory.js100%100%100%100%
   ContainerExposedDependency.js100%100%100%100%<

@alexander-akait alexander-akait merged commit b83041a into main Jun 13, 2026
66 checks passed
@alexander-akait alexander-akait deleted the perf/css-html-parser-codegen branch June 13, 2026 11:19
alexander-akait added a commit that referenced this pull request Jun 16, 2026
…the tokenizer

Decompose the CSS-Modules logic in lib/css/CssParser.js into focused helpers
and a node-type switch (walkSelectorList, emitComposesWithAnchor, the
Declaration visitor), and move CSS escape resolution into the tokenizer as lazy
accessors (Token#unescaped, Node#unescapedName) — superseding backslash-only
stripping with full unescaping, which also fixes hex escapes (e.g. \75 rl ->
url). Add CssIcssExportDependency codegen perf (resolveReferences context-hoist
and a transient composes-name index) plus assorted parser allocation cuts.

Rebased on #21181: keeps its orthogonal optimizations and supersedes its
stripBackslashes / property-name caching with the equivalents here.
alexander-akait added a commit that referenced this pull request Jun 16, 2026
…the tokenizer (#21196)

* refactor: decompose CSS-Modules parser and move escape handling into the tokenizer

Decompose the CSS-Modules logic in lib/css/CssParser.js into focused helpers
and a node-type switch (walkSelectorList, emitComposesWithAnchor, the
Declaration visitor), and move CSS escape resolution into the tokenizer as lazy
accessors (Token#unescaped, Node#unescapedName) — superseding backslash-only
stripping with full unescaping, which also fixes hex escapes (e.g. \75 rl ->
url). Add CssIcssExportDependency codegen perf (resolveReferences context-hoist
and a transient composes-name index) plus assorted parser allocation cuts.

Rebased on #21181: keeps its orthogonal optimizations and supersedes its
stripBackslashes / property-name caching with the equivalents here.

* Add changeset for CSS escape resolution fix
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