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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions