perf: build the experimental HTML parser at module scope - #21492
Conversation
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.
🦋 Changeset detectedLatest commit: 844ab4e 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 (492afb5). Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@492afb5
yarn add -D webpack@https://pkg.pr.new/webpack@492afb5
pnpm add -D webpack@https://pkg.pr.new/webpack@492afb5 |
eddc405 to
1b8acf4
Compare
Merging this PR will improve performance by 48.62%
|
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.
1b8acf4 to
844ab4e
Compare
Types CoverageCoverage after merging claude/htmk-syntax-toplevel-p5j1y3 into main will be
Coverage Report |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
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:
|
|
The
This PR only relocates 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 |
Summary
The experimental HTML tree-construction parser (
lib/html/syntax.js) declared all of its per-parse data insideparseHtml: 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). EveryparseHtmlcall 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 — soparseHtmlshrinks 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 aNamingsubsection 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.jstree-construction conformance suite andtest/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