Skip to content

Eliminate cold-load FOUT/CLS with metric-matched fallback fonts (size-adjust/ascent-override) #571

Description

@yumike

Summary

packages/viewer/src/fonts.css ships five @fontsource faces (Roboto 400/500/700, JetBrains Mono 400/500) at the default font-display: swap with no metric-matched fallback face. On a cold rw serve load the browser lays out text in the system fallback, then reflows the whole page when the web fonts arrive — a layout-shift (CLS) hit for every first visit in standalone mode.

Add @font-face fallback faces with size-adjust / ascent-override / descent-override / line-gap-override tuned so the fallback occupies the same space as the real font. The swap then causes zero reflow.

Why

  • Independent CLS / SEO / UX win for the whole page in standalone mode — the article currently visibly jumps on first load, comments or not.
  • It also removes the dominant trigger behind the inline-comment deep-link positioning machinery added in fix(viewer): keep deep-linked inline comment thread aligned to its highlight #570 (fix(viewer): keep deep-linked inline comment thread aligned to its highlight). That fix re-measures the thread's pin after a web-font reflow strands it; with the reflow eliminated, the cold-load deepLinkSettleSeq re-measure becomes a rare belt-and-suspenders path.

Scope / approach

  • Fallback faces for Roboto (Arial/system fallback) and JetBrains Mono (Menlo/Consolas/monospace fallback) — code blocks reflow too.
  • Generate metrics with a tool (Fontaine / @capsizecss/metrics + createFontStack, à la next/font) or hand-tune from published metrics. ~6–12 lines of build-time CSS, one file.
  • Standalone-only: embed.ts does not import fonts.css (the host owns fonts in Backstage), so this cannot regress the embedded/Backstage path.

Out of scope (related, separate)

Markdown images render as bare <img> with no width/height (crates/rw-renderer/src/html.rs), so a late image above a deep-linked highlight still reflows on cold load regardless of fonts. Emitting intrinsic image dimensions from the renderer would close that residual case and let more of the deep-link re-measure path retire — but it's a larger renderer change; tracking separately.

References

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions