Skip to content

Commit bb320df

Browse files
fylornclaude
andauthored
perf(fonts): serve Geist from the site instead of Google Fonts (#20)
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 <noreply@anthropic.com>
1 parent 7502d64 commit bb320df

4 files changed

Lines changed: 103 additions & 10 deletions

File tree

‎astro.config.mjs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ export default defineConfig({
106106
},
107107
vite: {
108108
plugins: [tailwindcss()],
109+
build: {
110+
// Font files are always emitted as files (src/components/Fonts.astro).
111+
// Vite would otherwise inline those under 4 KB, the smaller Unicode
112+
// subsets of Geist, into the stylesheet as base64, and every page would
113+
// download subsets it never uses before its first paint. Anything else
114+
// keeps the default limit.
115+
assetsInlineLimit: (file) => (/\.woff2?$/.test(file) ? false : undefined),
116+
},
109117
},
110118
build: {
111119
inlineStylesheets: 'auto',

‎src/components/Fonts.astro‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
// The site's fonts, Geist and Geist Mono, served from this site. They used to
3+
// come from Google Fonts, whose stylesheet blocks the first paint until it has
4+
// loaded; where Google cannot be reached, as in mainland China without a
5+
// proxy, a page then stayed blank for about 30 seconds.
6+
//
7+
// The files are the Fontsource builds of the Google Fonts releases, bundled by
8+
// Vite under hashed names. As on Google Fonts, each weight is split into
9+
// subsets by unicode-range, so a browser downloads only the subsets a page
10+
// uses (Latin, for these pages), and every face uses font-display: swap.
11+
// Chinese text is set in the system's own font (--font-sans in global.css), so
12+
// no CJK font is downloaded. Until Geist has loaded, text is set in a local
13+
// font adjusted to Geist's metrics ("Geist Fallback" in global.css), so the
14+
// swap does not move the layout.
15+
//
16+
// The Latin files of the weights on every page are preloaded: 400 for text,
17+
// 500 for navigation and buttons, 600 for headings, and Geist Mono 400 for the
18+
// labels above them. The Open Graph images are drawn from the same packages
19+
// (src/lib/og.ts).
20+
import "@fontsource/geist/400.css";
21+
import "@fontsource/geist/500.css";
22+
import "@fontsource/geist/600.css";
23+
import "@fontsource/geist/700.css";
24+
import "@fontsource/geist-mono/400.css";
25+
import "@fontsource/geist-mono/500.css";
26+
import geist400 from "@fontsource/geist/files/geist-latin-400-normal.woff2?url";
27+
import geist500 from "@fontsource/geist/files/geist-latin-500-normal.woff2?url";
28+
import geist600 from "@fontsource/geist/files/geist-latin-600-normal.woff2?url";
29+
import geistMono400 from "@fontsource/geist-mono/files/geist-mono-latin-400-normal.woff2?url";
30+
31+
const preload = [geist400, geist500, geist600, geistMono400];
32+
---
33+
34+
{preload.map((href) => <link rel="preload" href={href} as="font" type="font/woff2" crossorigin />)}

‎src/layouts/Base.astro‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import "~/styles/global.css";
33
import { getLang, t } from "~/i18n";
44
import Seo from "~/components/Seo.astro";
5+
import Fonts from "~/components/Fonts.astro";
56
import { ogImageAlt, ogImagePath, type OgCard } from "~/lib/og";
67
import type { JsonLd } from "~/lib/structured-data";
78
@@ -47,14 +48,7 @@ const GA_ID = "G-R8LX8NCHCJ";
4748
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
4849
<meta name="generator" content={Astro.generator} />
4950

50-
<link rel="preconnect" href="https://fonts.googleapis.com" />
51-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
52-
<link
53-
rel="stylesheet"
54-
href={lang === "zh-CN"
55-
? "https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;600;700&display=swap"
56-
: "https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap"}
57-
/>
51+
<Fonts />
5852

5953
<!--
6054
Auto-redirect to the user's preferred locale.

‎src/styles/global.css‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,65 @@
3030
--color-pink-500: #d6a95a; --color-pink-600: #b98a3f; --color-pink-700: #946b31; --color-pink-800: #74542b; --color-pink-900: #5f4526; --color-pink-950: #352413;
3131
--color-fuchsia-300: #e9cb8e; --color-fuchsia-400: #e5c07b; --color-fuchsia-500: #d6a95a;
3232

33-
--font-sans: "Geist", "Noto Sans SC", "PingFang SC", "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
34-
--font-mono: "Geist Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
33+
/* Geist is self-hosted (src/components/Fonts.astro). Chinese text uses the
34+
system's CJK font: PingFang on macOS and iOS, Microsoft YaHei on Windows,
35+
Noto Sans CJK on Linux and Android. */
36+
--font-sans: "Geist", "Geist Fallback", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
37+
--font-mono: "Geist Mono", "Geist Mono Fallback", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
38+
}
39+
40+
/* ---------- Fallback faces ---------- */
41+
/* Until Geist and Geist Mono have loaded, text is set in a local font scaled
42+
to their metrics, so that swapping them in does not reflow the page. For
43+
each weight, size-adjust is the ratio of the widths of the English text of
44+
this site's pages set in Geist and in the local font (Arial for 400 and
45+
500, Arial Bold for 600 and 700, Courier New for Geist Mono); the ascent,
46+
descent and line gap are Geist's, divided by size-adjust. Arial, Liberation
47+
Sans and Arimo share their metrics, as do Courier New, Liberation Mono and
48+
Cousine, so one set of figures serves macOS, Windows and Linux. */
49+
@font-face {
50+
font-family: "Geist Fallback";
51+
src: local("Arial"), local("ArialMT"), local("Liberation Sans"), local("LiberationSans"), local("Arimo");
52+
font-weight: 100 400;
53+
size-adjust: 102.26%;
54+
ascent-override: 98.28%;
55+
descent-override: 28.85%;
56+
line-gap-override: 0%;
57+
}
58+
@font-face {
59+
font-family: "Geist Fallback";
60+
src: local("Arial"), local("ArialMT"), local("Liberation Sans"), local("LiberationSans"), local("Arimo");
61+
font-weight: 500;
62+
size-adjust: 104.69%;
63+
ascent-override: 96%;
64+
descent-override: 28.18%;
65+
line-gap-override: 0%;
66+
}
67+
@font-face {
68+
font-family: "Geist Fallback";
69+
src: local("Arial Bold"), local("Arial-BoldMT"), local("Liberation Sans Bold"), local("LiberationSans-Bold"), local("Arimo Bold"), local("Arimo-Bold");
70+
font-weight: 600;
71+
size-adjust: 99.89%;
72+
ascent-override: 100.61%;
73+
descent-override: 29.53%;
74+
line-gap-override: 0%;
75+
}
76+
@font-face {
77+
font-family: "Geist Fallback";
78+
src: local("Arial Bold"), local("Arial-BoldMT"), local("Liberation Sans Bold"), local("LiberationSans-Bold"), local("Arimo Bold"), local("Arimo-Bold");
79+
font-weight: 700 900;
80+
size-adjust: 102.13%;
81+
ascent-override: 98.4%;
82+
descent-override: 28.88%;
83+
line-gap-override: 0%;
84+
}
85+
@font-face {
86+
font-family: "Geist Mono Fallback";
87+
src: local("Courier New"), local("CourierNewPSMT"), local("Liberation Mono"), local("LiberationMono"), local("Cousine");
88+
size-adjust: 99.98%;
89+
ascent-override: 100.52%;
90+
descent-override: 29.5%;
91+
line-gap-override: 0%;
3592
}
3693

3794
/* ---------- Base ---------- */

0 commit comments

Comments
 (0)