Skip to content

perf(parsers): ensureHfIds skips the parse when every element already has its id - #7069

Merged
miguel-heygen merged 6 commits into
mainfrom
perf/hf-ids-skip-stamped
Nov 3, 2026
Merged

miguel-heygen merged 6 commits into
mainfrom
perf/hf-ids-skip-stamped

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

What

ensureHfIds returns its input as it is, without a parse, when every element it would give an id already has a non-empty data-hf-id.

Why

The studio preview build calls ensureHfIds twice on the bundled document, and the bundle is already stamped (stampHfIds: true), so both calls parse and re-serialize the whole page to mint nothing. On a page with a large inline SVG that is most of the build's time in the server process, which blocks it for as long as it takes.

On a 20,000-path inline SVG (about 1.4 MB once stamped), one call on the stamped document takes a median of 249 ms on main and 33 ms with this change, so a preview build of such a page does about 430 ms less work.

Related work

The first step of making the preview build parse its document less often. The bundler and the other preview steps still parse on their own; they come next.

How

  • alreadyStamped(html) reads the start tags with the file's existing tag reader (startTags, the one stampHfIdsInPlace uses), from the <body> tag on (or the whole text for a fragment), and answers true only when each one is in EXCLUDED_TAGS or carries a non-empty data-hf-id among its own attributes. hasHfId reads attributes with quoted values masked (outsideQuotes), so an id-looking string inside another attribute's value never counts.
  • Any doubt answers false and takes the full parse, as before:
    • a document that does not open <html> with <head> (holding only meta, link, base, title, style, script) then </head> and <body>. The parse path's document.body read makes linkedom add a missing <head> or <body>, so its output always has both, and callers that re-parse it rely on that (the SDK's edit-and-undo round trips compare serialized documents);
    • in a fragment, a stray <body> (a fragment is checked from its first tag);
    • a <table>, whose parse can add elements the text has none of;
    • a repeated html/head/body;
    • markup the tag reader cannot follow;
    • an empty id;
    • an element inside a plain <template> without an id.
  • assignHfIds only mints ids for elements without one and never changes an existing id, so skipping it when nothing is missing changes no id.

Test plan

  • Unit tests added: hfIds.parseCount.test.ts counts parses. A fully stamped page comes back unchanged with no parse (fails on main: one parse, and the re-serialized text). A missing or empty id still parses once and mints, a stamped page without its <head> (or without </head>) still parses and gets both, elements before a stray <body> in a fragment get their ids, and an id inside a quoted value does not count.
  • The parsers, sdk and studio-server test suites pass in full (the sdk's edit-and-undo round trips caught the missing <head> on an earlier head of this branch); lint, format, tsc and the comment share pass.
  • Timing as above, median of 5 runs each on the same machine.
  • Comments follow CONTRIBUTING.md "Comments".

Size

About 85 lines (half of them the test): the first, self-contained step of a larger cut. The preview build's other parses (the bundler's and the later preview steps') are separate changes in other packages, each with its own parse-count test; folding them in would mix three packages in one review.

@heygengenesis heygengenesis Bot 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.

Request changes at 7cd5c0f9: one correctness blocker, and the fix is one line.

Blocker: in a fragment, alreadyStamped skips every element before a <body> tag

When the input is a fragment (isFragment is true), alreadyStamped still slices from the first body start tag it finds. Elements before that tag are never checked, and the body tag itself is never tested against IMPLIED. The wrapped parse on main keeps all of them inside the body and mints ids for them, so the fast path returns the input without ids that main would have added. That breaks the PR's own invariant ("a repeated html/head/body … answers false").

I diffed main against this head, comparing the minted id sets for the same input:

input main mints this PR
<head><title>t</title></head><body><div data-hf-id="a">x</div></body> (an HTML page that leaves out <html>, which is valid) head, title, body nothing (fast path)
<style>.a{}</style><section>hi</section><body><div data-hf-id="a">x</div></body> section, body nothing (fast path)
<p>y</p><body><div data-hf-id="a">x</div> p, body nothing (fast path)

Suggested fix, which I checked locally:

const body = isFragment(html) ? -1 : tags.findIndex((tag) => tag.name === "body");

With this change, a body tag inside a fragment hits IMPLIED and takes the full parse. All three cases above then match main, and hfIds.test.ts plus hfIds.parseCount.test.ts pass (44/44). Please add one of these cases to hfIds.parseCount.test.ts (it should parse once and mint).

Verified otherwise

  • hfIds.parseCount.test.ts fails on main: the stamped case gets one parse and a re-serialized string. It passes at this head.
  • Parsers package: everything passes except htmlParser.roundtrip and stableIds. Those two also fail on main in my sandbox because @hyperframes/core/generators isn't built there, so the failure isn't caused by this PR.
  • The rest of my main-vs-PR probe matched: composition and plain templates, CDATA in SVG, textarea/noscript/plaintext/iframe, comments containing tags, uppercase or unquoted DATA-HF-ID, a whitespace-only id, select/option, MathML, content after </html>.
  • Returning the raw input instead of linkedom's serialization is fine for the callers I checked (preview.ts, sourceMutation.parseSourceDocument, sdk parseMutable/buildDocument). It also stops timeline ids from rewriting files that are already stamped just because the serialization normalizes them.

Note: this is a perf PR, not a fix, so I didn't hold it to the fails without the fix: line. The parse-count test does fail on main.

@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 108 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Right, thanks for the three cases. Fixed at bb8f1a5f with your line: a fragment checks from its first tag, so a stray <body> in one is an implied tag and takes the full parse.

Added your <p>y</p><body><div data-hf-id="…">x</div> case to hfIds.parseCount.test.ts: it parses once and mints the <p>. It fails with the old line and passes now. hfIds.test.ts + hfIds.parseCount.test.ts pass (44/44, three runs), and lint, format and tsc are clean.

@heygengenesis heygengenesis Bot 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.

Request changes at bb8f1a5f. The fragment fix is right. The remaining blocker is a red CI check that comes from this PR.

Resolved: the fragment <body> slice

alreadyStamped now reads a fragment from its first tag, so a stray <body> hits IMPLIED and takes the parse. The new test, "checks a fragment from its first tag…", fails against the previous head 7cd5c0f9 and passes here. All three cases from my earlier review now match main. The parsers suite passes in full (1395 passed; the two files that failed before only needed core built). oxfmt --check is clean.

Blocker: the Comments check fails

RAISED comment share: packages/parsers/src/hfIds.ts is 6.1% (15/245), at the base 4.2% (9/213)
    6 comment line(s) added. Cut them, or move the explanation into a name or a type.

This check also failed at 7cd5c0f9, which I missed in the first review. To get back to ≤4.2%, hfIds.ts can keep at most about one of the six added comment lines. Candidates to cut:

  • the // A bundle stamped at build time… line in ensureHfIds;
  • the new // A fragment's every tag is body content… line;
  • the three-line alreadyStamped docblock;
  • the hasHfId docblock.

The names, together with the parse-count test descriptions, already carry most of what those lines explain. Cutting the six lines brings the file to 9/239 (3.8%).

I'll approve once the Comments check passes on the new head.

@miguel-heygen
miguel-heygen force-pushed the perf/hf-ids-skip-stamped branch from bb8f1a5 to f052915 Compare November 3, 2026 11:34
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

At f0529151: CI on bb8f1a5f was red in the sdk (5 edit-and-undo round trips). Those fixtures are stamped pages with no <head>. On main, ensureHfIds reads document.body, and linkedom then adds the missing <head>, so its output always has one; the fast path returned the page without it, and the sdk's later edit added it mid-round-trip.

Fix: the fast path now also needs the page to open <html> with <head> (only meta, link, base, title, style, script inside), then </head> and <body>; anything else takes the parse as before. New case in hfIds.parseCount.test.ts (a stamped page without <head>, and one without </head>, each parse once); it fails without the check. Full parsers, sdk and studio-server suites pass; the comment share is back under the base (10/252 vs 9/213).

@heygengenesis heygengenesis Bot 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.

Request changes at f0529151. The logic is sound. The blocker is formatting, and it fails CI again.

Blocker: hfIds.parseCount.test.ts fails oxfmt --check

On this head, Preflight (lint + format) fails in both Windows render verification and preview-regression, and the downstream Windows Tests/Render and preview-regression "Check results" failures follow from it. The log says packages/parsers/src/hfIds.parseCount.test.ts … Format issues found. The cause is the second ensureHfIds(...) call in the new "parses a stamped page not opening then " test (line 46), whose line is too long. oxfmt wants:

    ensureHfIds(
      `<!DOCTYPE html><html><head><title>t</title><body><p data-hf-id="hf-a1b2">x</p></body></html>`,
    );

Run npx oxfmt on that file. No logic change is needed.

Verified

  • The Comments ratchet blocker from my last review is resolved: the check passes, and five of the six added comment lines are gone.
  • opensHeadThenBody is conservative in the right direction. It requires <html>, then <head>, then only head-content tags, then a </head> (outside comments) before <body>. The </head> search starts after the last head tag's raw-text end, so a </head> inside a <script>/<style>/<title> doesn't count. Anything else takes the parse as before.
  • The fragment fix and its test are unchanged from bb8f1a5f.
  • Locally at this head, the parsers suite passes (1396 passed) and the SDK suite passes (709/709, including the round trips).

I'll approve once a formatted head passes preflight.

Comment thread packages/parsers/src/hfIds.ts Fixed
Comment thread packages/parsers/src/hfIds.ts Fixed

@heygengenesis heygengenesis Bot 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.

Approve at 9c9941a1. No blocker remains.

  • Formatting blocker resolved: oxfmt --check is clean on both files. Every workflow on this head passes: CI, Comments, CodeQL, preview-regression, Windows render verification, regression, PR captures and Player perf.
  • closesHead split: it behaves the same as before for well-formed input, and it now scans comments and </head> in a single left-to-right matchAll. A </head> inside a comment is consumed by the comment match and doesn't count, which resolves CodeQL's comment-replace finding. When the last head tag's raw text never ends (from < 0), it answers false. The new shape cases (unclosed head, a <div> in head, no body, a commented-out </head>) each take the parse.
  • Local runs at this head: parsers 1396 passed, SDK 709/709.
  • Carried forward: the fragment fix, the head-then-body gate, and the parse-count test that fails on main are all unchanged from what I verified on earlier heads.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 35753f3 Nov 3, 2026
81 checks passed
@miguel-heygen
miguel-heygen deleted the perf/hf-ids-skip-stamped branch November 3, 2026 13:33
@WaterrrForever WaterrrForever mentioned this pull request Nov 3, 2026
1 task done
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