PageSpeed Insights 一跑下去,手机版分数 55 分、FCP(First Contentful Paint)17.2 秒。用户要等 17 秒才看到第一个界面。

问题的根源在字体:不是图片,也不是 JavaScript。

先说结论

项目优化前优化后
fonts.css1,757 行(161KB)39 行(~2KB)
@font-face 宣告216 个3 个
字体文件数213 个 woff23 个 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

  1. 系统字体优先用在中文内文。PingFang TC / Microsoft JhengHei / Noto Sans CJK TC 的质量已经够好
  2. 自订字体只用在品牌元素:标题、logo、特殊 UI,而且要积极做 subset
  3. 自架优于 CDN。cache partitioning 杀掉了 Google Fonts 的跨站快取优势
  4. font-display: optional 用在中文内文,swap 只用在确定需要自订字体的品牌元素
  5. preload 关键字体,但只 preload 一两个真正需要的文件
  6. 注意 fonts.css 的大小。上百个 @font-face 宣告本身就是性能瓶颈,即使浏览器只下载其中几个 subset
  7. 工具推荐:真的需要自订 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