- date
- entry
- 018
- topic
- performance
- rev
- —
中文字型別用 webfont:一支 stylesheet 佔掉 800ms
這個站原本用 Google Fonts 載字型。Lighthouse 實測時,那支 stylesheet 單獨佔掉近 800 毫秒的首次繪製時間——在一個總共只有幾十 KB 的靜態站上。
那行原本長這樣,就是官方文件給的寫法:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=..." />
問題不在它多大——那支 CSS 本身只有幾 KB。問題在它是 render-blocking:瀏覽器必須先取得所有 stylesheet、建好 CSSOM,才敢畫第一個像素。在拿到它之前,畫面是空白的。
800 毫秒花在哪
它是第三方網域,所以在「下載幾 KB」之前還有一整套前置:
- DNS 查詢
fonts.googleapis.com - TCP 連線 + TLS 交握
- 取得那支 CSS
- 解析它,發現裡面指向
fonts.gstatic.com的字型檔——換一個網域,再走一次 1–3
preconnect 能省掉第 1、2 步的等待,但只能對已知網域先開通道,省不掉「拿到 CSS 才知道要抓哪些字型檔」這個順序相依。在行動網路上,這條鏈就是好幾百毫秒。
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
第二行的 crossorigin 不能省。字型是以匿名 CORS 模式抓取的,少了這個屬性,preconnect 開的連線跟後面實際要用的連線不是同一條,等於白開一條還多花一次交握。
讓它不要擋住繪製
第一個改動是把它從阻斷式改成非阻斷式:
<link rel="preload" as="style"
href="https://fonts.googleapis.com/css2?family=..."
onload="this.onload=null;this.rel='stylesheet'" />
<noscript>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=..." />
</noscript>
原理是 rel="preload" 只下載、不套用,所以不擋繪製;下載完的 onload
把 rel 改成 stylesheet,這時瀏覽器才套用它。this.onload=null
是防止某些瀏覽器在改了 rel 之後重複觸發。
<noscript> 那份是給關掉 JavaScript 的人——注意這個技巧依賴
onload 屬性執行,所以它不是「純 CSS 解法」。
代價是 FOUT:頁面會先用後備字型畫出來,字型載完再換一次,文字會跳動一下。display=swap
就是明確選擇這個行為——寧可先看到字、再換樣子,也不要盯著空白等。
但真正的修法是不要載中文字型
上面那些都是在優化怎麼載。對中文站來說,更根本的問題是該不該載。
一套拉丁字型涵蓋的字符是幾百個等級,做完子集化之後幾十 KB。一套完整的繁體中文字型要涵蓋的是上萬個字,檔案大小是 MB 等級。這中間差了三個數量級——它不是一個可以靠壓縮或優化解決的差距,是規模本身的差距。
現代的 CJK webfont 服務會用 unicode-range 把字型切成上百個分片,只抓頁面實際用到的那幾片。這確實有效,但也代表:
- 首屏文字散落在多少個分片,就要發多少個請求
- 使用者往下捲、遇到新的字,還會繼續發請求
- 每一片都要等 CSS 解析完才知道要抓
換來的是什麼?中文讀者的系統裡本來就有品質不錯的黑體。為了讓字看起來「像設計稿」而下載幾 MB,在一個以文字為主的站上是划不來的交易。
系統字型堆疊
所以中文交給系統,只有拉丁字母和等寬字保留 webfont:
--sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "PingFang TC",
"Noto Sans TC", "Microsoft JhengHei", "Helvetica Neue", sans-serif;
--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
順序是有意義的,瀏覽器逐字符往後找第一個有該字符的字型:
- IBM Plex Sans 放最前面——它只有拉丁字符,所以中文字會自動落到後面去。等於免費得到「英數用設計字型、中文用系統字型」的混排
- PingFang TC 蘋果系統、Noto Sans TC Android 與部分 Linux、Microsoft JhengHei Windows
-apple-system/BlinkMacSystemFont讓拉丁部分在沒載到 webfont 時用系統 UI 字型,比 Helvetica 更接近原生觀感
要接受的取捨
誠實講缺點:站在不同作業系統上長得不完全一樣。
PingFang 和 JhengHei 的字重、字寬、標點位置都有差異,同一段文字在 Mac 和 Windows 上的行數可能不同。如果你的設計依賴精確的行高與斷行位置,這個方案會讓你不舒服。
我的取捨是接受它。理由是這個站的內容是長篇文字,讀者花在讀的時間遠多於花在看排版的時間,而系統字型在各自的平台上都是為了長時間閱讀調校過的。
如果真的必須用指定的中文字型(品牌需求、標題視覺),折衷做法是只對標題用,並且自己做子集化——把該標題實際用到的那幾十個字抽出來打包,檔案會回到幾十 KB 的等級。內文維持系統字型。
怎麼確認有效
不要靠感覺。Lighthouse 的「消除阻塞轉譯的資源」那一項會直接列出哪支資源擋了多久,改動前後各跑一次就有數字。
重點是兩次都在同樣條件下跑——同一個網路節流設定、無痕視窗、關掉擴充功能。這跟部署後比對內容一樣:宣稱改善之前,先有一個可以重跑的量測。
如果只記得一件事
對中文站,字型不是效能優化題目,是規模題目。 載入方式再怎麼調,也改變不了「這套字型有上萬個字」這件事。先問要不要載,再問怎麼載。
修訂紀錄
- 首次發布