Skip to content

perf(fonts): serve Geist from the site instead of Google Fonts - #20

Merged
fylorn merged 1 commit into
mainfrom
seo2/self-hosted-fonts
Sep 25, 2026
Merged

fylorn merged 1 commit into
mainfrom
seo2/self-hosted-fonts

Conversation

@fylorn

@fylorn fylorn commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Every page loaded a stylesheet from fonts.googleapis.com in <head>. A stylesheet blocks the first paint until it arrives, so where Google cannot be reached (mainland China without a proxy) the request hangs until it times out, about 30 seconds, and the page stays blank until then. The preview browser used for this change cannot reach Google either: on the current main build, the request ran 30.0 s, failed, and no font loaded.

What changes

  • Self-hosted Geist. Geist 400/500/600/700 and Geist Mono 400/500 come from @fontsource/geist and @fontsource/geist-mono, already in devDependencies (5.3.0). They are the same files Google Fonts serves: Geist 1.800 and Geist Mono 1.701, with identical advance widths, outlines and kerning (compared with fontkit). Vite bundles them under hashed names, each weight split into subsets by unicode-range as on Google Fonts, so a page downloads only the Latin files; every face uses font-display: swap. New component: src/components/Fonts.astro, included by Base.astro.
  • Preload. The Latin files of Geist 400 (text), 500 (navigation, buttons), 600 (headings) and Geist Mono 400 (labels) are preloaded on every page, with crossorigin so the preload is reused.
  • No base64 fonts in the stylesheet. Vite inlines assets under 4 KB, which put the smaller subsets into the render-blocking CSS as base64 (the stylesheet grew by 82 KB when tried). vite.build.assetsInlineLimit now keeps .woff/.woff2 as files and leaves everything else at the default, so Astro's stylesheet and script inlining is unchanged.
  • Metric-adjusted fallbacks. Until Geist has loaded, text is set in Arial (Arial Bold for 600 and 700) and Courier New, with size-adjust, ascent-override, descent-override and line-gap-override so the swap does not reflow the page. Liberation Sans/Arimo and Liberation Mono/Cousine share those metrics, so the same figures apply on Linux. size-adjust is the width ratio over the English text of the site's 22 English pages, per weight.
  • Chinese text uses the system font: PingFang SC, Hiragino Sans GB, Microsoft YaHei, Noto Sans CJK SC, Noto Sans SC, Source Han Sans SC. The Chinese pages no longer download Noto Sans SC.
  • The preconnects to fonts.googleapis.com and fonts.gstatic.com are removed.

Checks

  • pnpm build (pnpm 10) passes; tsc --noEmit passes. No page in dist/ references fonts.googleapis.com or fonts.gstatic.com.
  • Served locally: the English and Chinese home pages request only same-origin fonts: Geist Latin 400/500/600/700 and Geist Mono Latin 400/500, four of them from the preloads. No Latin Extended, Cyrillic or CJK file is requested; the site's text has no Latin Extended characters.
  • Fallback metrics measured in the browser over the English text of every page: Geist Fallback is 1.0000 of Geist's width at 400, 500, 600 and 700, line boxes are equal (20.5 px at 16 px), and 149 of 150 long paragraphs wrap to the same number of lines in a 640 px column. Unadjusted Arial is 2.2% narrower at 400 and 4.5% at 500.
  • The stylesheet grows from 78 KB to 90 KB (13.8 KB to 15.5 KB gzipped) with the @font-face rules; it contains no data:font URL.
  • The Open Graph images (src/lib/og.ts reads its outlines from the same packages) are byte-identical to main's.
  • Internal links: 4,502, 0 broken. The JSON-LD is unchanged from main.
  • Merges cleanly with feat(seo): social preview images for the GitHub repositories #17, feat(core): server deployment on the Core page, the home page and the Core docs #18 and Lite page and docs: the current app, remote core, and the licensing contact #19 in either order.

🤖 Generated with Claude Code

Every page loaded a stylesheet from fonts.googleapis.com in <head>, and a
stylesheet blocks the first paint until it arrives. Where Google cannot be
reached, as in mainland China without a proxy, the request hangs until it
times out, about 30 seconds, and the page shows nothing until then.

- Geist 400/500/600/700 and Geist Mono 400/500 come from the Fontsource
  packages already in devDependencies (the same files as Google Fonts:
  version, advance widths, outlines and kerning match). Vite bundles them
  under hashed names with the subsets split by unicode-range, so a page
  downloads only the Latin files, and each face uses font-display: swap.
- The Latin files of Geist 400, 500 and 600 and Geist Mono 400 are
  preloaded from every page.
- Font files are never inlined into the stylesheet as base64: Vite would
  otherwise inline the subsets under 4 KB and every page would download
  them before its first paint.
- Until Geist has loaded, text is set in Arial (Liberation Sans or Arimo
  on Linux) and Courier New, scaled with size-adjust and the ascent,
  descent and line-gap overrides so that the swap does not reflow the
  page. The figures come from the English text of the site's pages.
- Chinese text uses the system's font (PingFang SC, Microsoft YaHei,
  Noto Sans CJK SC) instead of downloading Noto Sans SC.
- The preconnects to fonts.googleapis.com and fonts.gstatic.com are gone.
  The Open Graph images still read their outlines from the same packages
  and come out byte-identical.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@fylorn
fylorn merged commit bb320df into main Sep 25, 2026
1 check passed
@fylorn
fylorn deleted the seo2/self-hosted-fonts branch September 25, 2026 07:01
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