包包baobaolin.com
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」之前還有一整套前置:

  1. DNS 查詢 fonts.googleapis.com
  2. TCP 連線 + TLS 交握
  3. 取得那支 CSS
  4. 解析它,發現裡面指向 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" 只下載、不套用,所以不擋繪製;下載完的 onloadrel 改成 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 的「消除阻塞轉譯的資源」那一項會直接列出哪支資源擋了多久,改動前後各跑一次就有數字。

重點是兩次都在同樣條件下跑——同一個網路節流設定、無痕視窗、關掉擴充功能。這跟部署後比對內容一樣:宣稱改善之前,先有一個可以重跑的量測。

如果只記得一件事

對中文站,字型不是效能優化題目,是規模題目。 載入方式再怎麼調,也改變不了「這套字型有上萬個字」這件事。先問要不要載,再問怎麼載。

修訂紀錄

  1. 首次發布