Skip to content

perf(fonts): serve webfonts locally, drop Inter for system-ui - #48

Merged
IngTian merged 1 commit into
mainfrom
claude/local-fonts
Aug 12, 2026
Merged

perf(fonts): serve webfonts locally, drop Inter for system-ui#48
IngTian merged 1 commit into
mainfrom
claude/local-fonts

Conversation

@IngTian

@IngTian IngTian commented Aug 12, 2026

Copy link
Copy Markdown
Owner

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 with CannotFetchFontFile / 404 because Google 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.

The three files under src/assets/fonts/ are the exact latin subsets Astro had already downloaded. Variable fonts, one file per style, 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.

Provider config goes under options.variants, not at the top level — the family schema is strict and rejects variants there.

Verified against a genuinely cold cache: node_modules/.astro/fonts moved aside, not just the .astro/ copy, which is only a mirror. The build completes with Copying 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

before after
bytes 308KB 210KB
LCP 3010ms 2409ms
FCP 1956ms 1956ms
perf 92 96
CLS 0 0

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 NS body, 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

…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>
@IngTian
IngTian merged commit 4985ef0 into main Aug 12, 2026
1 check passed
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