---
title: "Google Fonts 国内加载慢怎么办：自托管与中文字体子集化"
description: "Google Fonts 国内加载慢、拖住首屏的原因，以及下载 woff2 自托管、unicode-range 子集化、preload 与 font-display 取舍、中文字体切片、缓存头配置和上线验证的具体步骤。"
url: https://cdnping.org/blog/self-host-google-fonts-china
language: zh-CN
published: 2026-10-25
category: "性能"
tags: ["前端性能","Web 字体","自托管","中文字体","缓存"]
source: cdnping.org
license: Free to quote and summarize with a link to the url above.
---
# Google Fonts 国内加载慢怎么办：自托管与中文字体子集化

Google Fonts 国内加载慢，原因在于页面要先连上 `fonts.googleapis.com` 拿 CSS，再连 `fonts.gstatic.com` 拿字体文件，这两个域名在不少国内网络下连接慢甚至失败，而那段 CSS 默认会阻塞渲染。解决办法是把字体文件下载下来和站点放在一起（自托管），再配合子集化和 `font-display`。下文依次讲原因、迁移步骤、中文字体处理和上线验证。

## 外链字体为什么会拖住首屏？

一条常见的引用写法是：

```html
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
```

浏览器处理它要经过这几步：

1. 解析到这个 `<link rel="stylesheet">`。外部样式表默认是渲染阻塞资源，CSS 没回来之前页面不会首次绘制。
2. 为 `fonts.googleapis.com` 做 DNS 解析、TCP 连接和 TLS 握手，然后下载 CSS。
3. CSS 里的 `@font-face` 指向 `fonts.gstatic.com`，又是一个新域名，再做一轮 DNS、TCP、TLS。
4. 字体文件下载完，文字才用目标字体渲染。

第 2、3 步是两次“首次连接”，和请求本身多大关系不大。网络好的时候各几十毫秒，网络差的时候可能是几秒，或者直接超时。第 1 步决定了最坏情况：CSS 卡住，整页白屏。`display=swap` 只管字体文件（第 4 步），管不了阻塞渲染的那份 CSS。

还有一个常被引用的旧理由已经不成立：“用公共字体 CDN 可以和别的网站共享缓存”。主流浏览器都已按站点分区缓存（Chrome 从 86 版开始），A 站缓存的字体，B 站用不上。对国内访客来说，外链 Google Fonts 已经没有缓存上的收益，只剩连接成本。

### 怎么测出这两个域名在你那里有多慢？

可以用下面三种方法实测：

- 打开 [cdnping 在线测速](/)，看公共前端 CDN 的“首次连接”一栏。它包含 DNS 和 TLS 握手，正好对应上面第 2、3 步的成本；“往返”是连接复用后 3 次请求的中位数。两者差得越多，说明“多一个域名”在你的网络里越贵。
- 打开自己的页面，DevTools → Network，勾选 Disable cache，筛选 `fonts.g`，看每个请求的 Timing 里 DNS Lookup、Initial connection、SSL 三段。
- 命令行可以用 curl 拆开看：

```bash
curl -o /dev/null -s -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n' \
  https://fonts.gstatic.com/
```

本文不给具体数字：结果取决于运营商、地区和时段，别人测的数字对你参考价值有限。cdnping 自 2026 年 10 月 17 日开始采集，历史还很短，测速口径见 [数据方法](/methodology)。如果整站在国内都慢，而不只是字体，可以先看 [静态网站国内访问慢怎么诊断](/blog/static-site-slow-in-china-diagnose)，DNS 层面的异常可以参考 [DNS 污染和 DNS 劫持怎么判断](/blog/dns-pollution-vs-hijacking-detect)。

## 自托管 Google Fonts 的步骤

Google Fonts 上的字体大多采用 SIL Open Font License 或 Apache License，允许自行分发。自托管前确认一下具体字体的许可，并把 LICENSE 文件和字体放在一起。

### 第 1 步：列出页面实际用到的字体和字重

```bash
grep -rn "fonts.googleapis\|fonts.gstatic" src/ public/ index.html
```

把每个 family 和 `wght` 记下来。很多项目引用了 300、400、500、700 四个字重，实际只用到 400 和 700，删掉不用的字重就能少几个请求。

### 第 2 步：拿到 woff2 文件

有三种方式，从省事到手动排列。

#### 方式 A：用 fontsource 的 npm 包（适合有构建流程的项目）

fontsource 把 Google Fonts 打包成 npm 包，按字重和字符子集拆好了文件，`@font-face` 也写好了：

```bash
npm i @fontsource/inter            # 静态字重
# 或可变字体版本
npm i @fontsource-variable/inter
```

```js
// main.js / main.ts
import '@fontsource/inter/400.css';
import '@fontsource/inter/700.css';
```

```css
body { font-family: 'Inter', system-ui, sans-serif; }
```

构建后字体文件会进入 `dist/assets/`，文件名带 hash。如果 `npm install` 本身就慢，先看 [npm 镜像源怎么设置](/blog/npm-registry-mirror-china-setup)。

#### 方式 B：构建时自动下载

Vite 项目可以用 `vite-plugin-webfont-dl` 这类插件，构建时把 HTML 里的 Google Fonts 链接下载下来、改写成本地路径，源码里的 `<link>` 不用动。注意构建机必须能访问 Google 的这两个域名，CI 跑在海外一般没问题。

#### 方式 C：手动下载

Google Fonts 的 CSS 接口会按 User-Agent 返回不同格式，带上现代浏览器的 UA 才会给 woff2：

```bash
UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'
curl -sA "$UA" 'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap' -o inter.css
grep -o 'https://fonts.gstatic.com/[^)]*' inter.css | while read u; do
  curl -sO "$u"
done
```

然后把 `inter.css` 里的 URL 改成你自己的路径，比如 `/fonts/xxx.woff2`。

### 第 3 步：写 @font-face，保留 unicode-range

Google 返回的 CSS 已经按 latin、latin-ext、cyrillic 等字符集拆成了多段，每段带 `unicode-range`。浏览器只下载页面上真正出现的字符所在的那一段。手动搬运时不要把这些段合并，只删掉确定用不到的（比如纯中文站可以删 cyrillic、greek、vietnamese）：

```css
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/inter-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
```

只写 woff2 就够了，当前主流浏览器都支持，不必再带 woff、ttf、eot。

### 第 4 步：preload 首屏字体，但别贪多

```html
<link rel="preload" href="/fonts/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
```

两个细节：`crossorigin` 必须写，即使是同域，否则预加载的文件不会被复用，会下载两次；只预加载首屏正文用的那一个文件。把所有字重都 preload 会和首屏图片、脚本抢带宽，反而更慢。

## font-display 选哪个？

| 取值 | 行为 | 适合 |
|---|---|---|
| `swap` | 立即用后备字体显示，字体到了再替换 | 正文、品牌字体，最常用 |
| `fallback` | 约 100ms 隐形期，约 3 秒内到达才替换，否则本次不换 | 正文字体，想减少晚到时的跳动 |
| `optional` | 约 100ms 内没到就不用，留给下次访问（已缓存） | 字体可有可无、对布局稳定要求高 |
| `block` | 最长约 3 秒隐形，再换后备 | 图标字体，不建议用于正文 |

`swap` 的代价是字体替换时的布局偏移（CLS）。可以用 `size-adjust`、`ascent-override` 让后备字体的度量接近目标字体，减小跳动：

```css
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
}
body { font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif; }
```

上面的百分比只是示例，具体值需要按字体计算。构建工具里有自动生成后备度量的插件，例如 unjs 的 fontaine，可以省去手算。

## 中文字体怎么办？

拉丁字体一个字重通常几十 KB，中文字体要覆盖几千到几万个字，完整文件常以 MB 计，不处理就直接引用会明显拖慢首屏。按需求从轻到重有三种做法。

### 能用系统字体就用系统字体

大多数正文场景，系统自带的中文字体已经够用，零下载：

```css
body {
  font-family: system-ui, -apple-system, 'PingFang SC', 'Hiragino Sans GB',
               'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC', sans-serif;
}
```

英文部分用自托管的拉丁字体，中文交给系统字体，额外下载只有拉丁字体那几个文件。

### 文字固定：按实际用到的字子集化

Logo、标题、按钮这类文字固定的场景，用 fonttools 只保留用到的字：

```bash
pip install fonttools brotli
# chars.txt 里放页面实际用到的所有字符
pyftsubset NotoSansSC-Bold.otf \
  --text-file=chars.txt \
  --flavor=woff2 \
  --layout-features='*' \
  --output-file=title-sc.woff2
```

几十个字的子集通常只有几 KB 到十几 KB。缺点是文案一改就要重新生成，最好放进构建脚本：从模板里提取字符，自动跑一遍。

### 文字不固定：按 unicode-range 切片

博客、文档这类内容随时变化的站点，做法和 Google Fonts 处理中日韩字体一样：把整套字体切成上百个小文件，每个带自己的 `unicode-range`，浏览器只下载页面上出现的字所在的分片。

- 直接用 fontsource 的中文包，例如 `@fontsource/noto-sans-sc`，它沿用了 Google 的切片方式，装上就能用。
- 想用其他中文字体，可以用专门的切片工具（例如开源的 cn-font-split）自己切，输出 woff2 分片和对应的 CSS。
- 自己规划切片时，常用的思路是把《通用规范汉字表》一级字表的 3500 个常用字按使用频率分到前面几片，生僻字放后面，这样一篇普通文章只触发少数几个分片。

切片多了以后请求数也多，所以站点最好开着 HTTP/2 或 HTTP/3，让这些请求复用同一个连接。

## 自托管后的缓存和响应头

字体自托管后，缓存头决定回访是否还要重新下载。构建产物文件名带 hash 时，可以放心设长缓存：

```nginx
location ~* \.woff2$ {
    types { font/woff2 woff2; }
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header Access-Control-Allow-Origin "*";
}
```

- `Content-Type` 用 `font/woff2`。
- 文件名不带 hash（比如方式 C 手动放到 `/fonts/` 的文件）就不要加 `immutable`，换字体时改文件名或加版本参数。各指令的含义见 [Cache-Control 缓存头怎么配](/blog/cache-control-headers-guide-cdn)。
- 字体放在和页面不同的域名上（比如单独的静态资源域名），字体请求属于跨域请求，必须返回 `Access-Control-Allow-Origin`，否则浏览器会拒绝使用。

如果站点本身放在海外托管、国内访问就慢，字体跟着站点走也快不起来。这时要看整站的分发方案：选公共 CDN 可以先看 [CDN 速度榜](/benchmark/cdn) 和 [国内怎么选公共 CDN](/learn/choose-public-cdn-china)；面向中国大陆境内的加速需要 ICP 备案，并经由持证服务商。

## 上线后怎么验证？

对照这张检查表逐项过一遍：

| 检查项 | 方法 | 预期 |
|---|---|---|
| 不再请求 Google 域名 | `grep -rn "fonts.googleapis\|fonts.gstatic" dist/`；DevTools 筛选 `fonts.g` | 没有结果 |
| 字体是 woff2 且同源 | Network 筛选 Font，看 Type 和 Domain | `font/woff2`，域名是自己的 |
| preload 被复用 | Console 无 “preloaded but not used” 警告；同一文件只下载一次 | 只有一条请求 |
| 只下载需要的分片 | 打开一篇中文文章，数 Font 请求 | 只有少数分片，不是整套 |
| 缓存头正确 | `curl -I https://你的域名/assets/xxx.woff2` | 有 `max-age=31536000`，hash 文件带 `immutable` |
| 布局偏移可接受 | Lighthouse 或 Performance 面板看 CLS | 字体替换时没有明显跳动 |
| 弱网下首屏不白 | DevTools 节流到 Slow 4G，刷新 | 先出后备字体的文字，不等字体 |

还有一个容易漏掉的地方：第三方组件（评论框、图表库、文档主题）可能自己带着 Google Fonts 的 `<link>` 或 `@import`。上线后在生产环境再筛一次 `fonts.g`，比只查源码更可靠。

## 改完之后对比一次

自托管之后，页面少了两个外部域名的连接，也去掉了一个会阻塞渲染的第三方样式表。可以用 [在线测速](/) 对比公共 CDN 域名和自己域名的首次连接耗时，再在 DevTools 里对比改动前后的首次绘制时间，确认实际省下了多少。网络层面的其他排查项可以参考 [开发者网络自检清单（2026 版）](/blog/developer-network-self-check-2026)。
