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";
-
-
-
+