繁體中文網站最容易被忽略的效能成本,常常不是第三方腳本,而是字型。完整 CJK 字型可能很大;若每位訪客都先下載整包,手機網路就得先端上一杯「等待咖啡」,文字才慢慢穩定下來。

這篇會帶你判斷是否真的需要 Web Font、理解字型子集化與 unicode-range 的差別,最後用自架 WOFF2 的流程把載入成本降下來。重點不是追求某個神奇檔案大小,而是讓字型策略符合網站內容與實際測量結果。

先判斷:你真的需要 Web Font 嗎?

如果網站是內容型部落格、管理後台或文件站,最划算的起點通常是系統字體優先。系統字體不需要額外下載,少了一趟網路請求,也少了一個可能延遲文字顯示的變數:

body {
	font-family:
		system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
		"PingFang TC", "Microsoft JhengHei", "Noto Sans CJK TC", sans-serif,
		"Apple Color Emoji", "Segoe UI Emoji";
}

macOS 與 iOS 常見的是 PingFang TC,Windows 常見的是 Microsoft JhengHei;實際結果仍會受作業系統、已安裝字型與瀏覽器影響。如果你不需要每台裝置都呈現完全相同的品牌字形,這一步可能比任何字型工具更有效。

三種字型策略怎麼選?

可以先用下面的方式縮小選擇:

情境建議做法主要取捨
內容型部落格內文使用系統字體載入成本低,但不同平台字形不完全一致
品牌官網或 Landing Page標題使用子集化 Web Font,內文使用系統字體兼顧美觀與速度,但要維護字元清單
必須固定品牌字形的產品 UI自架 WOFF2,搭配靜態或分片子集化控制力最高,也需要加入 build 流程

不要只因為「看起來專業」,就讓整篇內文載入完整的 Noto Sans TC。先把 Web Font 限制在 H1、H2 或少量 UI 元件,通常更容易取得可接受的速度與一致性。字型不是越多越有文化,請求數才不會因此變成詩。

為什麼繁中字型特別容易拖慢首屏?

中文字型需要涵蓋大量漢字、標點與不同字重,因此完整檔案可能遠大於 Latin 字型。一個網站若同時載入多個 CJK 字型族和多個字重,下載量很快就會累積;若使用外部服務,還要考慮連線、CSS 發現時機與字型檔本身的請求。

外部 CDN 不一定比較慢,自架也不保證比較快。實際結果會受到 CDN 位置、HTTP 版本、快取策略、CSS 何時被瀏覽器發現,以及頁面到底匹配到哪些字型分片影響。跨網站共用快取也不能當成必然優勢,所以應該把「外部服務」和「自架」都放進同一套實測裡比較。

字型子集化到底在做什麼?

子集化就是從完整字型中只保留網站需要的字元,再輸出較小的字型檔。HarfBuzz 的 hb-subset 文件也把它描述為縮減字型涵蓋的 codepoint,並移除不再需要的資料。

常見做法有兩種:

  1. 靜態子集化:掃描指定頁面或整個網站,只保留目前用到的字元。檔案通常更小,適合內容固定的 Landing Page;但內容更新後必須重跑。
  2. Unicode 分片:把字型切成多個檔案,在 CSS 的每組 @font-face 中用 unicode-range 指定負責的字元範圍。瀏覽器會根據頁面文字判斷要不要載入某個分片,但匹配後下載的是整個分片,不是單獨一個字。

這裡有個容易誤會的地方:WOFF2 格式和瀏覽器不會自動替你切檔。分片需要多組 @font-face、多個 WOFF2 網址,以及對應的 unicode-range。只有一個完整 WOFF2 和一條 @font-face,瀏覽器就只會把它當成一個檔案處理。

Google Fonts 會替你產生包含 unicode-range 的 CSS,某些字型也會依字元或語系切成多個檔案;你只放入 stylesheet 連結,就能使用它預先準備好的分片。自架則要自行產生分片與 CSS,例如使用 cn-font-split

本站曾以四種字型載入約 23 個 WOFF2 請求、總計約 1.78MB 作為改版前觀測值;後來改用自架子集,並讓內文回到系統字體,目前實際使用的 Web Font 減少到兩個檔案。這是本站某次測量,不是所有網站都會得到相同數字;分片數量減少,也不代表每個頁面一定少下載同樣多的位元組。

子集缺少的字也不會自動變成缺字方框。瀏覽器會沿著 font-family 堆疊尋找下一個可用字型,代價是罕見字可能和周圍文字有些風格差異。因此,子集化完成後要測試特殊人名、專有名詞、全形標點與程式碼符號。

自架子集化的實作流程

1. 收集真正會出現的字元

掃描文章、模板、導覽列、按鈕和錯誤訊息,收集漢字、全形標點、Latin 字母、數字與常用符號。不要只掃一篇文章,否則下一篇新增一個罕見字時,字型就會突然「失憶」。

Latin 部分可以參考字型服務提供的範圍,但仍要依網站實際內容確認,避免英文、貨幣符號或程式碼標點意外 fallback。若標題和內文使用不同字型,也應分別收集,避免把所有文章文字都塞進只套用於標題的字型。

2. 用工具產出 WOFF2

cn-font-split 適合把 CJK 字型切成可按範圍載入的分片;如果想依網站實際用字產出單一子集,可以使用 subset-font。這個 Node.js 套件以 HarfBuzz 的 subset 功能為基礎,會輸出指定格式的字型檔。

概念流程如下:

掃描 Markdown、模板與導覽文字
            ↓
收集 Latin、CJK 與符號
            ↓
subset-font 或 cn-font-split
            ↓
輸出 WOFF2 與必要的 CSS
            ↓
用瀏覽器與效能工具驗證

如果專案已經有腳本,讓它成為固定的 build 步驟會比手動操作可靠。例如本站可在新增內容後執行:

pnpm run subset-fonts
pnpm run build

3. 用 @font-face 控制載入行為

最基本的自架設定可以長這樣:

@font-face {
	font-family: "Noto Sans TC";
	src: url("/fonts/noto-sans-tc.woff2") format("woff2");
	font-weight: 400 700;
	font-style: normal;
	font-display: swap;
}

接著在 HTML 中把它套用到真正需要的元素,而不是只宣告卻全站使用:

<h1 class="hero-title">網站標題</h1>

font-display: swap 會先顯示 fallback 字型,字型載入完成後再替換,適合需要優先看見文字的標題。optional 則讓瀏覽器在載入不及時時更傾向維持 fallback;它可以降低字型替換造成版面變化的機會,但不是「完全不會 CLS」的保證。實際顯示時間仍由瀏覽器實作與網路狀況決定。

4. 把子集化接進 build 流程

新增文章或修改導覽列後,重新執行字型腳本,再進行 production build。原始字型可以放在本機快取,避免每次重跑都重新下載;真正重要的是讓「新增內容 → 重新產生子集 → 測量效能」成為固定流程。

5. 移除不再需要的 CDN 標籤

改成自架後,檢查 <head> 裡的 Google Fonts preconnectpreload、stylesheet 和 noscript 標籤。新舊兩套設定同時留下來,等於請瀏覽器喝兩杯咖啡再做同一件事——熱鬧,但沒有比較快。

常見錯誤與取捨

  • 直接上傳 OTF 或 TTF:Web Font 通常優先使用 WOFF2,因為壓縮效率較好且現代瀏覽器支援廣;仍要依你的瀏覽器支援範圍決定是否提供其他格式。
  • 以為分片等於只下載一個字unicode-range 只負責判斷某個分片是否匹配;匹配後通常會下載整個分片。
  • 只留下漢字、忘了 Latin 和符號:英文、數字、括號或程式碼符號可能改用不同字型,應把實際需要的字元一起納入。
  • 把整個網站都套品牌字體:先從標題或少量 UI 開始,否則字型下載量和替換風險都會快速增加。
  • 忽略子集缺字:fallback 能避免缺字方框,卻不會讓不同字型的字形風格自動一致;上線前要測試罕見字、專有名詞與標點。
  • 只看檔案大小、不測實際體驗:請用 PageSpeed Insights 或自己的效能驗收流程,觀察 LCP、CLS、文字是否及時出現,以及 Network 面板實際下載了哪些字型。

結語:先少載入,再追求一致

字型最佳化可以先記住三件事:

  1. 內容站先用系統字體,不要預設整包載入 CJK Web Font。
  2. 需要品牌字形時,只把標題或 UI 子集化,並依需求選擇單一子集或 Unicode 分片。
  3. font-display、fallback 和實際 Core Web Vitals 測量確認取捨,不要只看理論檔案大小。

下一步很簡單:先記下改動前的數據,挑一個頁面做子集化,再比較 LCP、CLS、字型請求數和實際文字顯示。先測一頁就好,不必一口氣把整個網站的字型 pipeline 變成考古現場。測完再來一杯,這次是慶功。

參考資料