Google Fonts 国内加载慢怎么办:自托管与中文字体子集化

博客 / 性能

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

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

  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 拆开看:
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 和 Domainfont/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 版)。

相关文章

继续阅读