perf(fonts): serve webfonts locally, drop Inter for system-ui - #48
Merged
Conversation
…m-ui
Two things the owner green-lit after the merge, and they compound.
── LOCAL FILES, NOT GOOGLE. The build fetched every face from fonts.gstatic.com, so
every build — including the deploy — depended on a third party being consistent at
that moment. It already bit us: a CI run failed with CannotFetchFontFile / 404
because Google had rotated JetBrains Mono's file hashes mid-run (the request ended
BYaTNPxDcwgknk-4, the live URL ended BYRbKPx3cwgknk-6nFg). A retry passed, but the
same failure on a push to main would leave the site stale with nothing wrong in the
repo, and nothing in the codebase to point at.
The files committed under src/assets/fonts are the exact latin subsets Astro had
already downloaded. They are variable fonts — one file per style covering the whole
weight range — so `weight` is declared as a range and two files cover Fraunces'
400/500/600 in both styles. Stored in src/ rather than public/, because Astro copies
them into the output itself and public/ would ship them twice.
Provider config goes under `options.variants`, not at the top level: the family
schema is strict and rejects `variants` there ("Unrecognized key"). Verified against
a genuinely cold cache — node_modules/.astro/fonts moved aside, not just the .astro/
copy, which is only a mirror — and the build completes with "Copying fonts (3
files)" and no network fetch.
── INTER IS GONE; the body face is the system UI stack. Two reasons that agree. It is
what the live site has always actually rendered — SF on a Mac — because no webfont
loaded at all until a few commits ago, and the owner's preference has consistently
been for what he was already reading. And it was the most expensive face here: 98KB
of the payload for a sans-serif nearly indistinguishable from SF at body size.
MEASURED, on a production build, against the state that just shipped:
bytes 308KB -> 210KB
LCP 3010ms -> 2409ms
FCP 1956ms -> 1956ms (unchanged; FCP is bound by the render-blocking CSS)
perf 92 -> 96 (median of 4; one run scored 84 on a TBT-488ms outlier
while the other three measured TBT 0)
CLS 0 -> 0
96 is now above the 94 the site scored before any of this, while actually rendering
its own type — and the vendored payload is 121KB against 222KB fetched.
All four type roles verified painting on the built site: Georgia on the display,
.SF NS on body copy, JetBrains Mono [custom] on the mono, Fraunces [custom] still on
the Appendix sub-headlines and the italic ledes.
Gates: 808 tests green, a11y / best-practices / SEO 100, and the geometry sweep 0
findings over 196 combinations x 3 scroll positions — re-run because a new body face
changes every text metric on the site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Two follow-ups to #46, both green-lit after the merge. (Replaces #47, which was cut from the pre-squash branch and so conflicted with
main's squashed history.)Local font files, not Google
The build fetched every face from
fonts.gstatic.com, so every build — including the deploy — depended on a third party being consistent at that moment. It already bit us: a CI run on #46 failed withCannotFetchFontFile/ 404 because Google rotated JetBrains Mono's file hashes mid-run (the request endedBYaTNPxDcwgknk-4, the live URL endedBYRbKPx3cwgknk-6nFg). A retry passed, but the same failure on a push tomainwould leave the site stale with nothing wrong in the repo.The three files under
src/assets/fonts/are the exact latin subsets Astro had already downloaded. Variable fonts, one file per style, soweightis declared as a range and two files cover Fraunces' 400/500/600 in both styles. Stored insrc/rather thanpublic/because Astro copies them into the output itself.Provider config goes under
options.variants, not at the top level — the family schema is strict and rejectsvariantsthere.Verified against a genuinely cold cache:
node_modules/.astro/fontsmoved aside, not just the.astro/copy, which is only a mirror. The build completes withCopying fonts (3 files)and no network fetch.Inter dropped; body is the system UI stack
Two reasons that agree: it is what the live site has always actually rendered — SF on a Mac — because no webfont loaded at all until a few commits ago, and it was the most expensive face here (98KB for a sans-serif nearly indistinguishable from SF at body size).
Measured, production build, against what just shipped
Median of 4 runs; one scored 84 on a TBT-488ms outlier while the other three measured TBT 0. 96 is above the 94 the site scored before any of this — while actually rendering its own type. Vendored payload is 121KB against 222KB fetched.
All four type roles verified painting on the built site: Georgia display,
.SF NSbody, JetBrains Mono[custom]mono, Fraunces[custom]still on the Appendix sub-headlines and italic ledes.Gates
808 tests green · a11y / best-practices / SEO 100 · geometry sweep 0 findings over 196 combinations × 3 scroll positions, re-run because a new body face changes every text metric on the site.
🤖 Generated with Claude Code