Skip to content

perf(desk): show the desk without waiting for web fonts - #7

Merged
kanywst merged 6 commits into
mainfrom
perf/lcp
Oct 24, 2026
Merged

kanywst merged 6 commits into
mainfrom
perf/lcp

Conversation

@kanywst

@kanywst kanywst commented Oct 24, 2026

Copy link
Copy Markdown
Member

Production Lighthouse (mobile) put LCP at 4.5 s, and 1.7 s of that was element render delay: the home seal had loaded but the desk stayed hidden until document.fonts.ready. The desk now shows as soon as the script runs. The ResizeObserver from #5 re-measures items as the fonts swap in, and the intro's final framing is computed when it is used instead of up front.

Locally, with applied (devtools) throttling, mobile scores 98 with LCP 2.2 s. Local fonts arrive almost at once, so the gain shows on the network rather than here. e2e: 43 passed.

The desk stayed hidden until document.fonts.ready, so on a slow connection the
home seal (the page's largest image) had already loaded and then waited about
1.7 s to paint. Items are re-measured by the ResizeObserver as fonts swap in,
and the intro's final framing is computed when it is used.
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 60054cd9-7a69-4df8-a8b9-d320114021a3
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/scripts/desk.ts
@claude

claude Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Reviewed the diff (src/scripts/desk.ts, e2e/desk.spec.ts) against injection, CSP, shared DOM, input/focus and sync. Nothing found. There's no new HTML sink, inline style or host. The desk and list views behave as before. The late re-frame goes through go(), which snaps under prefers-reduced-motion, and it is cancelled by every input path via cancelIntro. It is also skipped when a live frame is open (liveFrom). Sync is untouched.

Without the fonts.ready wait, the instant (and reduced-motion) opening fit was
computed from fallback-font sizes and never revisited. If the visitor hasn't
moved yet, the desk is framed again from the real sizes.

Raised in Claude review on #7.
Comment thread src/scripts/desk.ts
// Once the fonts have swapped in, frame the desk again from the real sizes, unless the visitor
// has already moved, a site is open, or the intro is still about to compute it anyway.
document.fonts.ready.then(() => {
if (!html.classList.contains("desk") || !untouched || introTimer || liveFrom) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

introTimer is never reset when the timeout fires (line 567 only assigns it; only cancelIntro zeroes it). Once the 650 ms pull-back has run, introTimer stays truthy, so || introTimer here returns early for good. If the fonts arrive after 650 ms (slow network, the case this PR is for), the re-frame is skipped. The intro already computed overview() from the pre-font sizes, so the desk stays mis-framed.

Reset it inside the timeout callback:

introTimer = window.setTimeout(() => { introTimer = 0; go(overview(), { stiffness: 2.2 }); }, 650);

untouched already covers input. Also, halt() calls cancelIntro(), so a desk→list→desk toggle sets untouched = false without any camera input. That is harmless, but the flag then means "halted or touched", not "untouched".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 533abf0.

A fired timer stayed truthy, so the post-font re-frame was skipped whenever the
fonts arrived after the intro, which is the slow-network case it exists for.

Raised in Claude review on #7.
Comment thread src/scripts/desk.ts
// has already moved, a site is open, or the intro is still about to compute it anyway.
document.fonts.ready.then(() => {
if (!html.classList.contains("desk") || !untouched || introTimer || liveFrom) return;
go(openingView(), { stiffness: 2.2 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

openingView() reads boxes, vw and vh, which are only refreshed by measure() or by the ResizeObserver (~line 604). document.fonts.ready resolves as a microtask when the font load finishes, which is before the next rendering step. ResizeObserver callbacks are delivered in that rendering step, after layout. So this handler can run while boxes still holds the pre-swap sizes. It would then re-frame from the same stale data and the fix would do nothing, depending on timing.

Suggested fix: re-measure here before framing, so it doesn't depend on observer ordering:

document.fonts.ready.then(() => {
  if (!html.classList.contains("desk") || !untouched || introTimer || liveFrom) return;
  measure();
  go(openingView(), { stiffness: 2.2 });
});

(measure() rebuilds the whole boxes map from offsetWidth/offsetHeight, which forces a synchronous layout and so picks up the swapped fonts.) The same applies to the intro timer callback at ~line 567: it runs 650ms after setMode, so it is probably fine there. Still, if you want the framing correct, measure() before overview() is cheap.

Minor: with the intro running, a font swap that lands after the timer has fired (introTimer === 0, untouched still true) re-targets the pull-back mid-flight. That is harmless, but it is a second animated move the visitor did not ask for. It is instant under reduced motion, because go honours it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 9fcc82c: measure() and the minimap run before the re-frame.

@kanywst
kanywst merged commit b981dcc into main Oct 24, 2026
8 checks passed
kanywst added a commit that referenced this pull request Oct 24, 2026
Without the fonts.ready wait, the instant (and reduced-motion) opening fit was
computed from fallback-font sizes and never revisited. If the visitor hasn't
moved yet, the desk is framed again from the real sizes.

Raised in Claude review on #7.
kanywst added a commit that referenced this pull request Oct 24, 2026
A fired timer stayed truthy, so the post-font re-frame was skipped whenever the
fonts arrived after the intro, which is the slow-network case it exists for.

Raised in Claude review on #7.
kanywst added a commit that referenced this pull request Oct 24, 2026
The ResizeObserver might not have run yet when fonts.ready resolves.

Raised in Claude review on #7.
@kanywst
kanywst deleted the perf/lcp branch October 24, 2026 14: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.

1 participant