Skip to content

fix(core): the fonts gate starts loading declared faces no laid-out text uses yet - #6415

Merged
miguel-heygen merged 2 commits into
heygen-com:mainfrom
jbernard077:fix/fonts-gate-loads-unused-faces
Oct 26, 2026
Merged

miguel-heygen merged 2 commits into
heygen-com:mainfrom
jbernard077:fix/fonts-gate-loads-unused-faces

Conversation

@jbernard077

Copy link
Copy Markdown
Contributor

What

waitForFonts in packages/core/src/runtime/afterFonts.ts forces one layout and awaits document.fonts.ready before composition scripts run. Layout only starts loading the faces that laid-out text uses, so a face used solely by text a script builds has not started loading when ready resolves, and the script measures the fallback face (title centred from the wrong width, underline drawn to the wrong length).

After the forced layout, the gate now calls load() on every entry in document.fonts whose status is still unloaded, then keeps awaiting ready under the existing FONT_WAIT_TIMEOUT_MS race. A face that never loads still ends in the existing timeout warning and the runtime_font_wait_timeout diagnostic. A rejected load() (a missing font file) is swallowed and falls through to that same path, so one broken face cannot stop composition scripts from running.

Cost

Faces a page declares but never uses will now load once at startup, bounded by the same timeout. Pages whose declared faces are all used by static text see no change: those faces were already loading.

Tests

packages/core/src/runtime/entry.afterFonts.test.ts, two cases added:

  • a declared face no laid-out text uses: load() is called once on it and not on an already-loaded face, and the scripts run only after that face and ready resolve;
  • a declared face whose load() rejects: the scripts still run at the timeout and the diagnostic reports the families still loading.

Both fail on the unpatched gate and pass with it. The file's 13 existing cases pass unchanged. Also run: core typecheck (both configs), runtime artifact build, repo lint, runtime preview-guard lint, the runtime contract and behavior lanes.

Fixes #5112

🤖 Generated with Claude Code

…ext uses yet

Before composition scripts run, waitForFonts forces one layout and awaits
document.fonts.ready. A face that only script-built text uses has not started
loading at that point, so ready does not wait for it and the script measures
the fallback face. Start every face still unloaded after the forced layout,
under the existing timeout; a load that rejects falls through to the same
timeout warning instead of stopping the scripts.

Refs heygen-com#5112

Co-Authored-By: Claude Fable 5.1 <[email protected]>

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, it is exactly the change we discussed on #5112, and the PR description makes the cost easy to see.

What works well:

  • afterFonts.ts:18-20 starts only the unloaded faces and leaves their promises to the existing ready/timeout race, so the gate stays bounded and a broken face can't throw.
  • I ran your repro's shape in Chromium against the base and head waitForFonts: a face only script-built text uses measured 324.4 px (fallback) at script time on the base and 465.4 px (the loaded face) with this change. A 404 face ends in error without delaying anything; a face whose server never answers ends at the 5 s timeout, as the Cost section says.
  • Both new tests fail when the loading loop is removed, so they guard the fix.

One thing to fix before merge:

  • The Comments check is red: afterFonts.ts goes from 6.4% to 8.0% comment lines, and the ratchet only lets that share go down. Keeping the added comment lines at zero is enough, for example replacing the comment above the forced layout and the new three-line comment with a single line:
    // Layout starts the faces laid-out text uses; load() starts the rest so ready covers them too.

Optional:

  • In entry.afterFonts.test.ts, the timeout test could include a face whose load() never settles (load: vi.fn(() => new Promise(() => {}))). Today, changing the loop to await face.load() still passes all 15 tests; that face would catch it.

Happy to approve once Comments is green.

Verdict: COMMENT
Reasoning: The fix is correct and verified in Chromium, but the PR turns the Comments check red with three added comment lines; approve once that is trimmed.

@jbernard077

Copy link
Copy Markdown
Contributor Author

Done in the second commit: the comment above the forced layout is now your one-liner and the three added lines are gone, so the file's comment share is below the base (the ratchet passes locally against the merge base). I also took the optional test: the timeout case now includes a face whose load() never settles, and it fails if the loop awaits load(), as you predicted.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, both points are done. The comments are down to one line (afterFonts.ts:13), so Comments no longer fails, and the timeout test now includes a face whose load() never settles. It also expects Silent Face in the timeout diagnostic's loadingFamilies, which pins that the gate still ends at the 5 s limit with such a face. Every check is green at this head.

The fix itself is unchanged from my last review, where I checked it in Chromium: a face used only by script-built text now measures with the loaded font.

Verdict: APPROVE
Reasoning: The fix matches what we agreed on #5112, both review points are addressed, and all checks pass at this head.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 26, 2026
Merged via the queue into heygen-com:main with commit 36a24ae Oct 26, 2026
82 checks passed
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Oct 26, 2026
Merge 8 upstream commits (85dcc19..7d6c02f: heygen-com#6418, heygen-com#5273, heygen-com#6245, heygen-com#5593,
heygen-com#5447, heygen-com#6415, heygen-com#6373, heygen-com#6375). Keep the fork's Windows-only ci.yml. For
cancellableProcess.ts take upstream's heygen-com#5273, which supersedes the fork's own
744a620 port (same StringDecoder design, but the output limit counts raw
bytes). Record the batch in docs/DECISIONS.md, mark heygen-com#6373/heygen-com#6375/heygen-com#6415/heygen-com#6418
as merged, and advance the commit watermark to 7d6c02f.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@jbernard077
jbernard077 deleted the fix/fonts-gate-loads-unused-faces branch October 26, 2026 18:31
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.

fonts gate before composition scripts only covers faces that static markup already uses

2 participants