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