Preload only the H1 and body faces - #1139
Merged
Merged
Conversation
On a throttled phone the CDN splits bandwidth between concurrent requests rather than honoring priority, so every preloaded font races the hero poster, the LCP element. Measured on production (Lighthouse mobile, devtools throttling), blocking Archivo italic moved LCP -228ms and JetBrains Mono -224ms, each more than PostHog's whole library. - JetBrains Mono is no longer preloaded, and gets a real monospace fallback stack. next/font's generated fallback for it was local(Arial), a proportional font that would misalign code while the face loads. - Archivo is split into two next/font instances so its italic can skip the preload (next/font preloads per instance). Both register the real family name, Archivo, so the browser merges them and every existing font-style: italic still selects the true italic. Nothing else changes. - Archivo normal STAYS preloaded. Un-preloading it was measured: the hero subhead wraps to two lines in the fallback and three in Archivo, so the swap pushed the CTAs and demo down, CLS 0.206 on a 375x812 phone. Local production build, devtools throttling (HTTP/1.1, directional): home LCP 2,429 -> 2,176ms. CLS across the font swap, measured with a layout-shift observer: home 0, docs 0, a blog post with early italics 0.0003. e2e/home-bundle.spec.ts now asserts exactly these two preloads by family and style, that the unpreloaded italic still merges into Archivo, and that mono never falls back to a proportional font. Each was mutation-tested and fails alone on its own regression.
blove
enabled auto-merge (squash)
September 23, 2026 15:32
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
2 tasks
blove
added a commit
that referenced
this pull request
Sep 23, 2026
The Inter comment in layout.tsx and the FONTS note in ui.css both said
next/font registers families under a hashed name. That was true of older
next/font; next/font 16 registers the real name — the built CSS emits
`@font-face{font-family:Inter}` and `--font-diagram:"Inter","Inter Fallback"`.
#1139's italic split depends on exactly that.
The conclusions still hold, so behaviour is unchanged: theme.css's
`Inter, system-ui` stack only names the family, and the font file exists only
because next/font loads it. Remove the loader and diagrams still fall back to
system-ui. Only the stated mechanism was wrong.
blove
added a commit
that referenced
this pull request
Sep 23, 2026
The Inter comment in layout.tsx and the FONTS note in ui.css both said
next/font registers families under a hashed name. That was true of older
next/font; next/font 16 registers the real name — the built CSS emits
`@font-face{font-family:Inter}` and `--font-diagram:"Inter","Inter Fallback"`.
#1139's italic split depends on exactly that.
The conclusions still hold, so behaviour is unchanged: theme.css's
`Inter, system-ui` stack only names the family, and the font file exists only
because next/font loads it. Remove the loader and diagrams still fall back to
system-ui. Only the stated mechanism was wrong.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stops preloading the two fonts the hero doesn't need, which were racing the LCP image on phones.
Why fonts, when the question was JavaScript
This came out of digging into the JS that blocks LCP. Module-level attribution of the homepage's 236KB (gz) of required scripts, from production source maps:
JavaScript is fetched at Low priority, yet blocking all of it on prod saved ~1,040ms. So the CDN splits bandwidth rather than honoring priority, and a KB of font costs as much as a KB of JS. Measured on prod (Lighthouse mobile, devtools throttling, HTTP/2, 5 runs each,
*/ingest/*blocked):Each of those two fonts costs more LCP than PostHog does, with no analytics trade-off.
Change
preload: false, plus a real monospace fallback stack.next/font's generated fallback for it waslocal(Arial)— a proportional font — which would have misaligned code on docs pages while the face loads.next/fontinstance withpreload: false.next/fontpreloads per instance, so this is the only way to skip just the italic. Both instances register the real family nameArchivo(checked in the built CSS), so the browser merges them: every existingfont-style: italicstill gets the true italic, and no CSS changes.What I deliberately did not ship
Un-preloading Archivo normal too was built and measured. It's the biggest gain (local LCP 1,803ms), but the hero subhead wraps to 2 lines in the fallback and 3 in Archivo, so the swap pushed the CTAs and the whole demo down: CLS 0.206 on a 375×812 phone, in every run. That's over the 0.1 rollback line set before starting. Recovering it would need the reflow solved first, for example by reserving the subhead's height. That's a separate decision.
Lighthouse reported that variant's CLS as 0.001: its trace missed the swap. The 0.206 comes from a
layout-shiftobserver spanning the whole font swap.Measured (local production build, directional)
Local serves HTTP/1.1, so these are relative only; prod is re-measured after deploy.
Guards (
e2e/home-bundle.spec.ts)@font-facevia the CSSOM, so the wrong face can't pass on count alone. It reads the live DOM becausenext devinjects preloads at hydration, while production writes them into the HTML.Archivo:document.fonts.load('italic 400 16px Archivo')must find an italic face. This catches a futurenext/fontthat hashes family names again, which would silently make every italic faux.All three were mutation-tested. Each regression — italic back in the preloaded instance, italic instance removed, mono back on the Arial fallback — fails only its own guard.
Verification
nx test website,nx lint website,nx build website*/ingest/*blocked. Rollback if LCP doesn't improve or CLS > 0.1.🤖 Generated with Claude Code