Google Fonts 国内加载慢怎么办:自托管与中文字体子集化
Google Fonts 国内加载慢,原因在于页面要先连上 fonts.googleapis.com 拿 CSS,再连 fonts.gstatic.com 拿字体文件,这两个域名在不少国内网络下连接慢甚至失败,而那段 CSS 默认会阻塞渲染。解决办法是把字体文件下载下来和站点放在一起(自托管),再配合子集化和 font-display。下文依次讲原因、迁移步骤、中文字体处理和上线验证。
外链字体为什么会拖住首屏?
一条常见的引用写法是:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
浏览器处理它要经过这几步:
- 解析到这个
<link rel="stylesheet">。外部样式表默认是渲染阻塞资源,CSS 没回来之前页面不会首次绘制。 - 为
fonts.googleapis.com做 DNS 解析、TCP 连接和 TLS 握手,然后下载 CSS。 - CSS 里的
@font-face指向fonts.gstatic.com,又是一个新域名,再做一轮 DNS、TCP、TLS。 - 字体文件下载完,文字才用目标字体渲染。
第 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 拆开看:
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 日开始采集,历史还很短,测速口径见 数据方法。如果整站在国内都慢,而不只是字体,可以先看 静态网站国内访问慢怎么诊断,DNS 层面的异常可以参考 DNS 污染和 DNS 劫持怎么判断。
自托管 Google Fonts 的步骤
Google Fonts 上的字体大多采用 SIL Open Font License 或 Apache License,允许自行分发。自托管前确认一下具体字体的许可,并把 LICENSE 文件和字体放在一起。
第 1 步:列出页面实际用到的字体和字重
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 也写好了:
npm i @fontsource/inter # 静态字重
# 或可变字体版本
npm i @fontsource-variable/inter
// main.js / main.ts
import '@fontsource/inter/400.css';
import '@fontsource/inter/700.css';
body { font-family: 'Inter', system-ui, sans-serif; }
构建后字体文件会进入 dist/assets/,文件名带 hash。如果 npm install 本身就慢,先看 npm 镜像源怎么设置。
方式 B:构建时自动下载
Vite 项目可以用 vite-plugin-webfont-dl 这类插件,构建时把 HTML 里的 Google Fonts 链接下载下来、改写成本地路径,源码里的 <link> 不用动。注意构建机必须能访问 Google 的这两个域名,CI 跑在海外一般没问题。
方式 C:手动下载
Google Fonts 的 CSS 接口会按 User-Agent 返回不同格式,带上现代浏览器的 UA 才会给 woff2:
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):
@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 首屏字体,但别贪多
<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 让后备字体的度量接近目标字体,减小跳动:
@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 计,不处理就直接引用会明显拖慢首屏。按需求从轻到重有三种做法。
能用系统字体就用系统字体
大多数正文场景,系统自带的中文字体已经够用,零下载:
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 只保留用到的字:
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 时,可以放心设长缓存:
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 缓存头怎么配。 - 字体放在和页面不同的域名上(比如单独的静态资源域名),字体请求属于跨域请求,必须返回
Access-Control-Allow-Origin,否则浏览器会拒绝使用。
如果站点本身放在海外托管、国内访问就慢,字体跟着站点走也快不起来。这时要看整站的分发方案:选公共 CDN 可以先看 CDN 速度榜 和 国内怎么选公共 CDN;面向中国大陆境内的加速需要 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 版)。