Skip to content

perf: build the experimental HTML parser at module scope - #21492

Merged
alexander-akait merged 4 commits into
mainfrom
claude/htmk-syntax-toplevel-p5j1y3
Jul 24, 2026
Merged

perf: build the experimental HTML parser at module scope#21492
alexander-akait merged 4 commits into
mainfrom
claude/htmk-syntax-toplevel-p5j1y3

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

Summary

The experimental HTML tree-construction parser (lib/html/syntax.js) declared all of its per-parse data inside parseHtml: the static quirks-mode public-id tables, ~55 tree-construction helpers, the 21 insertion-mode handlers, and the per-parse state (open/AFE stacks, insertion mode, head/form pointers, quirks, fragment, table/token scratch). Every parseHtml call therefore re-allocated the tables and re-created every helper as a fresh closure. This PR moves them to module scope — holding the per-parse state in module-level variables reset at the top of each parse, the same single-active-parse pattern the node/attribute columns already use — so parseHtml shrinks to a thin driver (state reset + fragment setup + the tokenizer callback loop). Behavior is unchanged. For a build parsing many HTML files this cuts parse time ~25% and peak parse memory ~15–18%. Also adds a Naming subsection to the coding standards (AGENTS.md). n/a issue.

What kind of change does this PR introduce?

perf (with a small docs addition to AGENTS.md).

Did you add tests for your changes?

No new tests — this is a behavior-preserving refactor with no new branches. It is covered by the existing test/html5lib.spectest.js tree-construction conformance suite and test/HtmlSyntax.unittest.js; locally I verified byte-identical serialized output over an adversarial corpus plus a state-reset-contamination check (parsing each case after a state-leaving parse matches parsing it fresh).

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 structure only; no public API or option changes.

Use of AI

AI (Claude) was used to perform the mechanical relocation of the helpers/state to module scope, to build the differential + reset-contamination validation harness, and to run the before/after CPU and memory benchmarks. All changes were reviewed and validated (byte-identical parser output) before committing.


Generated by Claude Code

The quirks-mode public-id table (QUIRKY_PREFIXES/QUIRKY_EXACT), the
isQuirky predicate, and the pure adjustSvgTag helper were declared inside
parseHtml, so the 57-element array, the Set, and both closures were
re-allocated on every parse. None of them close over per-parse state, so
move them to module scope.
…dule scope

mkEl, effParent, isScopeBoundary, sameAttrs, isAllWs, the MathML/HTML
integration-point predicates, and the foreign-attribute adjusters reference
only module-level column state and their arguments, yet were declared inside
parseHtml and thus re-allocated as fresh closures on every parse. Move them
to module scope; behavior is unchanged (verified byte-identical over a
branch-diverse serialized corpus).
Add a Naming subsection to the coding standards: spell names out in full,
with a short allowlist of established/spec abbreviations (ast, ns, afe, …)
and loop indices as the only exceptions.
Copilot AI review requested due to automatic review settings July 24, 2026 16:41

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.

@changeset-bot

changeset-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 844ab4e

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

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

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

Install it locally:

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

@alexander-akait
alexander-akait force-pushed the claude/htmk-syntax-toplevel-p5j1y3 branch from eddc405 to 1b8acf4 Compare July 24, 2026 16:46
Copilot AI review requested due to automatic review settings July 24, 2026 16:46

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 24, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 48.62%

⚠️ 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

⚡ 9 improved benchmarks
✅ 207 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory benchmark "devtool-eval", scenario '{"name":"mode-development-rebuild","mode":"development","watch":true}' 330.4 KB 130.4 KB ×2.5
Simulation unit benchmark "html-parser-document-unit", parseHtml (fragment, tr context) 169.1 ms 97.4 ms +73.61%
Simulation unit benchmark "html-parser-document-unit", parseHtml (skip text) 795.1 ms 519.3 ms +53.1%
Simulation unit benchmark "html-parser-document-unit", process (no visitors) 942.4 ms 656.9 ms +43.46%
Simulation unit benchmark "html-parser-document-unit", process (Element+Comment visitors) 948.8 ms 663.7 ms +42.96%
Simulation unit benchmark "html-parser-document-unit", parseHtml 969.1 ms 682.2 ms +42.06%
Memory benchmark "future-defaults", scenario '{"name":"mode-production","mode":"production"}' 9.3 MB 7.5 MB +23.15%
Memory unit benchmark "html-parser-document-unit", parseHtml (skip text) 818.6 KB 672.6 KB +21.71%
Memory benchmark "devtool-eval-source-map", scenario '{"name":"mode-production","mode":"production"}' 8 MB 6.6 MB +20.29%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/htmk-syntax-toplevel-p5j1y3 (844ab4e) with main (2043f6e)

Open in CodSpeed

parseHtml declared its ~55 tree-construction helpers, the 21 insertion-mode
handlers, runMode/dispatch, and all per-parse state as locals, so every parse
re-created them as fresh closures. Lift the helpers and mode handlers to
module scope and hold the per-parse state (open/afe stacks, insertion mode,
head/form pointers, quirks, fragment, table/token scratch) in module-level
variables reset at the top of each parse — the same single-active-parse
pattern the module-level node/attribute columns already use.

parseHtml shrinks to a thin driver (state reset + fragment setup + the
tokenizer callback loop). Output is byte-identical over a 92-case
adversarial corpus with an added reset-contamination check (parsing each
case after a state-leaving parse matches parsing it fresh).

Warm throughput improves ~22% on small documents and ~11% on large ones;
per-parse transient allocation drops (no per-parse closures), with steady-
state retained heap unchanged.
@alexander-akait
alexander-akait force-pushed the claude/htmk-syntax-toplevel-p5j1y3 branch from 1b8acf4 to 844ab4e Compare July 24, 2026 16:52
Copilot AI review requested due to automatic review settings July 24, 2026 16:52

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 claude/htmk-syntax-toplevel-p5j1y3 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,

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.52%. Comparing base (2043f6e) to head (844ab4e).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #21492      +/-   ##
==========================================
- Coverage   93.53%   93.52%   -0.01%     
==========================================
  Files         619      619              
  Lines       73195    73202       +7     
  Branches    21098    21083      -15     
==========================================
+ Hits        68460    68461       +1     
- Misses       4735     4741       +6     
Flag Coverage Δ
css-parsing 25.64% <ø> (+0.17%) ⬆️
html5lib 27.08% <ø> (+0.03%) ⬆️
integration 89.70% <ø> (+<0.01%) ⬆️
test262 43.14% <ø> (+0.14%) ⬆️
unit 46.24% <ø> (+<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.

Copy link
Copy Markdown
Member Author

The runtimes (bun) check is failing here due to pre-existing Bun-runtime flakiness, not this PR. Evidence:

  • It also fails on main at this PR's base commit 2043f6e (run 30054325146) — same job red there while all integration jobs pass.
  • Across three re-runs on this branch it crashed three different ways, each a native engine crash on a test unrelated to this change:
    1. jest worker "exited unexpectedly" — internalSerializables.unittest.js
    2. Aborted (core dumped) (SIGABRT, exit 134) — after TestCasesMinimizedSourceMap.longtest.js
    3. Illegal instruction (core dumped) (SIGILL, exit 132)

This PR only relocates lib/html/syntax.js internals to module scope (behavior-preserving); all 93,899 tests pass on the node runtime, codecov/patch is 100%, and CodSpeed shows the parser ~42–74% faster with no regressions. Ordinary module-scope declarations can't produce a SIGILL/SIGABRT in JavaScriptCore, and the same job is red on unchanged main.

I've stopped re-running it, since it's non-deterministic infra rather than anything fixable in this diff. Happy to re-run once more if a maintainer thinks it'll settle, but it looks like the known best-effort Bun runtime rather than a blocker introduced here.


Generated by Claude Code

@alexander-akait
alexander-akait merged commit 492afb5 into main Jul 24, 2026
142 of 146 checks passed
@alexander-akait
alexander-akait deleted the claude/htmk-syntax-toplevel-p5j1y3 branch July 24, 2026 18:19
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