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