From 231429e9ff7f4147327f4618f8460fddd1b70559 Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:08:22 +0800 Subject: [PATCH] perf(fonts): serve Geist from the site instead of Google Fonts Every page loaded a stylesheet from fonts.googleapis.com in , 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 --- astro.config.mjs | 8 +++++ src/components/Fonts.astro | 34 +++++++++++++++++++++ src/layouts/Base.astro | 10 ++----- src/styles/global.css | 61 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 src/components/Fonts.astro diff --git a/astro.config.mjs b/astro.config.mjs index a2e204b..bbbbdba 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -106,6 +106,14 @@ export default defineConfig({ }, vite: { plugins: [tailwindcss()], + build: { + // Font files are always emitted as files (src/components/Fonts.astro). + // Vite would otherwise inline those under 4 KB, the smaller Unicode + // subsets of Geist, into the stylesheet as base64, and every page would + // download subsets it never uses before its first paint. Anything else + // keeps the default limit. + assetsInlineLimit: (file) => (/\.woff2?$/.test(file) ? false : undefined), + }, }, build: { inlineStylesheets: 'auto', diff --git a/src/components/Fonts.astro b/src/components/Fonts.astro new file mode 100644 index 0000000..7ca1dbe --- /dev/null +++ b/src/components/Fonts.astro @@ -0,0 +1,34 @@ +--- +// The site's fonts, Geist and Geist Mono, served from this site. They used to +// come from Google Fonts, whose stylesheet blocks the first paint until it has +// loaded; where Google cannot be reached, as in mainland China without a +// proxy, a page then stayed blank for about 30 seconds. +// +// The files are the Fontsource builds of the Google Fonts releases, bundled by +// Vite under hashed names. As on Google Fonts, each weight is split into +// subsets by unicode-range, so a browser downloads only the subsets a page +// uses (Latin, for these pages), and every face uses font-display: swap. +// Chinese text is set in the system's own font (--font-sans in global.css), so +// no CJK font is downloaded. Until Geist has loaded, text is set in a local +// font adjusted to Geist's metrics ("Geist Fallback" in global.css), so the +// swap does not move the layout. +// +// The Latin files of the weights on every page are preloaded: 400 for text, +// 500 for navigation and buttons, 600 for headings, and Geist Mono 400 for the +// labels above them. The Open Graph images are drawn from the same packages +// (src/lib/og.ts). +import "@fontsource/geist/400.css"; +import "@fontsource/geist/500.css"; +import "@fontsource/geist/600.css"; +import "@fontsource/geist/700.css"; +import "@fontsource/geist-mono/400.css"; +import "@fontsource/geist-mono/500.css"; +import geist400 from "@fontsource/geist/files/geist-latin-400-normal.woff2?url"; +import geist500 from "@fontsource/geist/files/geist-latin-500-normal.woff2?url"; +import geist600 from "@fontsource/geist/files/geist-latin-600-normal.woff2?url"; +import geistMono400 from "@fontsource/geist-mono/files/geist-mono-latin-400-normal.woff2?url"; + +const preload = [geist400, geist500, geist600, geistMono400]; +--- + +{preload.map((href) => )} diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 45bb836..b5f2f63 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -2,6 +2,7 @@ import "~/styles/global.css"; import { getLang, t } from "~/i18n"; import Seo from "~/components/Seo.astro"; +import Fonts from "~/components/Fonts.astro"; import { ogImageAlt, ogImagePath, type OgCard } from "~/lib/og"; import type { JsonLd } from "~/lib/structured-data"; @@ -47,14 +48,7 @@ const GA_ID = "G-R8LX8NCHCJ"; - - - +