PageSpeed Insights 一跑下去,手机版分数 55 分、FCP(First Contentful Paint)17.2 秒。用户要等 17 秒才看到第一个界面。
问题的根源在字体:不是图片,也不是 JavaScript。
先说结论
| 项目 | 优化前 | 优化后 |
|---|---|---|
| fonts.css | 1,757 行(161KB) | 39 行(~2KB) |
| @font-face 宣告 | 216 个 | 3 个 |
| 字体文件数 | 213 个 woff2 | 3 个 woff2 |
| CJK body 字体 | Noto Sans TC(web font) | 系统字体 |
| CJK heading 字体 | Noto Serif TC(web font) | 系统宋体 |
做法很暴力:砍掉所有 CJK 网页字体,改用系统字体。只留 Crimson Pro 给英文标题。
听起来很退步?知乎、Medium、Matters 都这样做。
中文网页字体为什么这么重
英文字体一个 woff2 大概 20-50KB,因为只有几十个字母。
中文不一样。繁体中文常用字就有几千个,完整的 Pan-CJK 字型(像 Noto Sans CJK)涵盖六万多个字符,单一字重的源文件可以到 15-40MB。
Google Fonts 的解法是把大文件切成上百个小块,用 CSS 的 unicode-range 让浏览器只下载页面上出现的字元。这套机制很聪明:Google 用机器学习分析哪些字常一起出现,把它们塞进同一块,一般页面只会触发 5-15 个 subset 下载,大概 250KB-1MB。
但问题在别的地方。
结构性瓶颈:161KB 的 CSS,不是字体文件
fonts.css 有 1,757 行,光是 @font-face 宣告就 216 个。这个 CSS 文件本身就是 161KB。
浏览器在 parse 完这份 CSS 之前,不会开始渲染任何东西。在手机的 4G 网络上,光是下载 + parse 这份 CSS 就可能吃掉好几秒,字体文件还没开始下。
三套字体加起来:
- Noto Sans TC:107 个 unicode-range subset(body 用)
- Noto Serif TC:110 个 subset(标题 CJK 用)
- Crimson Pro:3 个 subset(标题英文用)
前两套各破百个 @font-face。就算浏览器只下载其中一小部分,光是那份巨大的 CSS manifest 就已经够慢了。
大站怎么处理 CJK 字体?
动手之前先做了一轮 Deep Research,问了四个 AI model 同样的问题。结论出奇一致:
高流量 CJK 网站几乎都用系统字体做内文。
- 知乎:PingFang SC → Microsoft YaHei → 系统 sans-serif。零网页字体
- Medium:英文内文用 Charter(自订字体),CJK 走系统 fallback
- Matters.news:以系统字体为主,搭配少量自订字体
2022 年开始,Chrome、Safari、Firefox 都实施了 cache partitioning:在 A 网站下载的 Google Fonts 快取,B 网站看不到。曾经「大家共用 CDN 快取」的优势已经不存在了。
跨站快取没了,自架 CJK web font 的性能成本就变成纯粹的负担。
font-display: optional vs swap
字体加载行为,有个容易搞错的选择。
font-display: swap 是很多教程的默认建议:先显示系统字体,等网页字体下载完再「换上去」。问题是 CJK 字体的字元宽度跟系统字体差很多,换的瞬间整个版面会跳一下。Google 叫这个 CLS(Cumulative Layout Shift),会直接扣 Core Web Vitals 分数。
font-display: optional 不一样。它给字体 100 毫秒的窗口:在这 100ms 内下载完了(或者已经在快取里),就用网页字体;没赶上就永远用系统字体,不会跳。
对中文内文来说,optional 搭配系统字体 fallback 是最安全的选择。版面不会跳,用户也不会注意到字体差异。
保留的 Crimson Pro(英文标题字体)就用 optional + preload。文件只有几十 KB,大部分情况 100ms 内就载完了。
实际怎么改
四个文件,改动不大。
1. 字体堆叠
/* body:系统黑体 */
--font-sans: 'PingFang TC', 'Noto Sans CJK TC', 'Microsoft JhengHei',
system-ui, -apple-system, sans-serif;
/* 标题:Crimson Pro (英文) + 系统宋体 (中文) */
--font-serif: 'Crimson Pro', 'Songti TC', 'PMingLiU', Georgia, serif;
macOS/iOS 会拿到苹方(PingFang TC),Windows 拿到微软正黑(Microsoft JhengHei),Android 拿到 Noto Sans CJK TC。三个都是各平台上最好的繁体中文黑体,而且是零下载,用户的装置上本来就有。
2. fonts.css 瘦身
从 1,757 行砍到 39 行。只留 Crimson Pro 三个 subset(Latin、Latin-ext、Vietnamese),全部改 font-display: optional。
3. 加 preload
在 <head> 最前面加一行:
<link rel="preload" href="/fonts/crimson-pro-latin.woff2"
as="font" type="font/woff2" crossorigin />
让浏览器在 parse CSS 之前就开始下载 Crimson Pro,配合 optional 的 100ms 窗口,大多数用户看到的标题就是 Crimson Pro。
4. 清理
删掉 213 个 noto-*.woff2 文件。public/fonts/ 从两百多个文件变成三个。
取舍
这个做法不是完美的。
Windows 标题的中文部分会 fallback 到新细明体(PMingLiU),质感不太好。macOS 上的宋体 TC(Songti TC)就漂亮多了。如果这个差异不能接受,可以考虑把标题也改成 sans-serif,或者用 cn-font-split 之类的工具只切几百个常用字的 subset。
品牌一致性会稍微下降。不同装置上看到的字体不完全一样。但读者多半不会注意到字体差异,会注意到的是页面要等 17 秒才出现。
2026 年版 CJK 字体 checklist
- 系统字体优先用在中文内文。PingFang TC / Microsoft JhengHei / Noto Sans CJK TC 的质量已经够好
- 自订字体只用在品牌元素:标题、logo、特殊 UI,而且要积极做 subset
- 自架优于 CDN。cache partitioning 杀掉了 Google Fonts 的跨站快取优势
font-display: optional用在中文内文,swap只用在确定需要自订字体的品牌元素- preload 关键字体,但只 preload 一两个真正需要的文件
- 注意 fonts.css 的大小。上百个
@font-face宣告本身就是性能瓶颈,即使浏览器只下载其中几个 subset - 工具推荐:真的需要自订 CJK 字体,
cn-font-split(Rust/WASM)可以做到 Google Fonts 等级的智慧切割,而且支持 Vite/Webpack 整合
延伸阅读
小企鹅的经验
penchan.co 是自架网站,CJK 字体的处理是真的踩过。一开始顺手丢 Google Fonts,发现字体文件大、首屏慢,后来改走系统字体 + 自架 subset 才把 LCP 拉回来。对中文站来说,默认用 PingFang / Noto Sans CJK 这条路,比追求自订字体实际得多。
常见问题
Q: 为什么不用 Google Fonts CDN?
2022 年起各大浏览器实施 cache partitioning,跨站快取优势已消失。自架 + 系统字体是目前性能最好的做法。
Q: 系统字体在不同装置看起来会不一样吗?
会,但差异不大。macOS/iOS 用苹方、Windows 用微软正黑、Android 用 Noto Sans CJK。三者都是高质量黑体,阅读体验差异极小。
Q: font-display: optional 跟 swap 差在哪?
swap 会先显示系统字体再跳成网页字体,造成版面跳动(CLS)。optional 给字体 100ms 加载,没赶上就直接用系统字体,完全不会跳。
整理:Penna|小企鹅 Penchan