font-display 與字型預載:消除文字閃爍與版面跳動
font-display 決定 字型還沒載入完時,文字怎麼顯示。這一個屬性同時影響 LCP 與 CLS,對 中文網站 尤其關鍵,因為中文字型動輒數 MB。
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: swap;
}兩個時間窗
要理解五個值,先理解瀏覽器把載入期間切成的兩段:
| 期間 | 說明 |
|---|---|
| 阻塞期(block period) | 這段時間內若字型未載入完,文字 完全不顯示 |
| 交換期(swap period) | 這段時間內若字型載入完,立刻換上 |
| 之後 | 字型才載入完的話,這次造訪就不用了 |
五個值的差別就是這兩段各有多長。
五個值
| 值 | 阻塞期 | 交換期 | 行為 |
|---|---|---|---|
auto | 依瀏覽器 | 依瀏覽器 | 通常等同 block |
block | 約 3 秒 | 無限 | 先空白,載入完換上 |
swap | 0 | 無限 | 立刻顯示備援,載入完換上 |
fallback | 約 100 毫秒 | 約 3 秒 | 短暫空白,超時就放棄換 |
optional | 約 100 毫秒 | 0 | 短暫空白,沒趕上就整次造訪不用 |
該選哪一個
中文網站預設選 swap
font-display: swap;它保證 文字立刻顯示(用系統字型),字型載入完再換掉。使用者至少讀得到內容。
代價是換字時會閃一下、可能造成版面位移,那可以用下面的 度量對齊 解決。
不要用 block(或讓它變成 auto)
block 在字型載入完成前 完全不顯示文字,最長 3 秒。
英文字型可能只是一瞬間,但中文字型要載入數 MB,那段空白可能長達數秒。使用者面對一片空白的頁面,而 LCP 也會一併被算進去(文字沒繪製,就沒有最大內容元素)。
auto 在多數瀏覽器上的行為等同 block,所以 不要不寫這個屬性。
optional:完全不要位移
@font-face {
font-family: "MyFont";
src: url("/fonts/myfont.woff2") format("woff2");
font-display: optional;
}這是 CLS 最安全的選擇
optional 的交換期是 0,字型如果在極短時間內沒載入完,這次造訪就一直用備援字型,之後也不會換。
所以 完全不會有換字造成的版面位移。載入完的字型會存進快取,下次造訪就用得到。
代價是使用者第一次造訪可能看不到您的品牌字型。品牌識別沒那麼要緊的話,這是很划算的取捨。
fallback:兩者之間
短暫空白(約 100 毫秒,通常察覺不到),之後給 3 秒的交換期。3 秒內載入完就換,沒趕上就放棄。
font-display: fallback;適合「希望盡量用品牌字型,但不要讓使用者等太久」的情況。
預載字型
字型的下載通常晚一步,瀏覽器要先解析 CSS、算出有元素用到這個字型,才會開始下載。
<link
rel="preload"
as="font"
type="font/woff2"
href="/fonts/myfont.woff2"
crossorigin
>crossorigin 是必填,即使是同網域
<!-- 錯誤:會下載兩次 -->
<link rel="preload" as="font" href="/fonts/myfont.woff2">字型的請求 一律以匿名跨來源模式發出(這是規格規定,即使字型放在同一個網域)。預載時不加 crossorigin,瀏覽器會認為預載的請求與實際使用的請求是兩個不同的請求,結果下載兩次,完全失去預載的意義。
只預載一個
預載多個字重會反效果
<!-- 四個字重互相搶頻寬,LCP 反而變差 -->
<link rel="preload" as="font" href="/fonts/regular.woff2" crossorigin>
<link rel="preload" as="font" href="/fonts/bold.woff2" crossorigin>
<link rel="preload" as="font" href="/fonts/light.woff2" crossorigin>
<link rel="preload" as="font" href="/fonts/italic.woff2" crossorigin>只預載首屏一定會用到的那一個,通常是正文的常規字重。標題用的粗體通常不必,標題文字量少,晚一點換字影響很小。
用第三方字型服務時
<!-- 提早建立連線 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 字型 CSS 帶上 display 參數 -->
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap"
rel="stylesheet"
>網址的 display=swap 參數
Google Fonts 產生的 CSS 會依這個參數在每個 @font-face 裡加上 font-display。一定要加,不加的話預設是 auto(也就是 block)。
減少換字時的版面跳動
備援字型與目標字型的 字寬與行高不同,換字的瞬間文字重新排列,把下方內容推移,這計入 CLS 。
解法是宣告一個 調整過度量的備援字族:
/* 以系統字型為來源,調整度量讓它接近目標字型 */
@font-face {
font-family: "FallbackAdjusted";
src: local("Microsoft JhengHei"), local("PingFang TC");
size-adjust: 97%;
ascent-override: 88%;
descent-override: 12%;
line-gap-override: 0%;
}
body {
/* 目標字型 → 調整過的備援 → 一般備援 */
font-family: "MyFont", "FallbackAdjusted", sans-serif;
}| 屬性 | 調整什麼 |
|---|---|
size-adjust | 整體字級的縮放比例 |
ascent-override | 基線以上的高度 |
descent-override | 基線以下的高度 |
line-gap-override | 行距 |
怎麼算出這些數字
理想做法是量測兩種字型的實際度量再算比例。實務上可以:
- 用瀏覽器開發者工具比較兩種字型下同一段文字的高度。
- 調整
size-adjust直到兩者的行數與總高度一致。 - 用 Rendering 面板的 Layout Shift Regions 確認換字時不再閃藍色。
也有工具能自動產生這組數值。
三個屬性一起用
一份完整的字型設定:
<head>
<!-- 1. 預載首屏的一個字型檔 -->
<link rel="preload" as="font" type="font/woff2" href="/fonts/main-400.woff2" crossorigin>
<link rel="stylesheet" href="/styles.css">
</head>/* 2. 目標字型,用 swap 保證文字立刻顯示 */
@font-face {
font-family: "MyFont";
src: url("/fonts/main-400.woff2") format("woff2");
font-weight: 400;
font-display: swap;
/* 只載入實際用到的字碼範圍 */
unicode-range: U+4E00-9FFF, U+0020-007E;
}
@font-face {
font-family: "MyFont";
src: url("/fonts/main-700.woff2") format("woff2");
font-weight: 700;
font-display: swap;
unicode-range: U+4E00-9FFF, U+0020-007E;
}
/* 3. 調整過度量的備援,減少換字時的位移 */
@font-face {
font-family: "FallbackAdjusted";
src: local("Microsoft JhengHei"), local("PingFang TC");
size-adjust: 97%;
ascent-override: 88%;
descent-override: 12%;
}
body {
font-family: "MyFont", "FallbackAdjusted", sans-serif;
}unicode-range 的分片做法見 中文字型子集化 。
不要用 JavaScript 控制字型載入
這是過時的做法
早期沒有 font-display 時,常見的做法是用程式碼偵測字型載入完成再加上一個類別:
// 過時:現在用 font-display 就好
document.fonts.ready.then(() => {
document.documentElement.classList.add('fonts-loaded');
});現在 font-display 已經涵蓋這個需求,而且不需要額外的 JavaScript。多一段程式碼反而增加 INP 的風險。
document.fonts API 仍有用途,例如:要在字型載入後才畫 canvas 文字。但純粹的文字顯示不需要它。
這個站的做法
這個站完全不用網頁字型
body {
font-family:
system-ui, -apple-system, "Segoe UI", "Noto Sans TC",
"PingFang TC", "Microsoft JhengHei", sans-serif;
}用系統字型堆疊的結果:
| 好處 | 說明 |
|---|---|
| 沒有額外請求 | 不必下載任何字型檔 |
| 沒有 FOIT 或 FOUT | 文字第一次繪製就是最終樣子 |
| 沒有換字造成的位移 | CLS 完全不受字型影響 |
不必設 font-display | 沒有 @font-face 就沒這個問題 |
| 不必預載 | 少一個要調的優先權 |
這是一個文件站可以做的取捨,內容的可讀性遠比字型的獨特性重要。省下來的力氣放在 圖片 與內容上,回報更大。
有品牌字型需求的專案則照上面「三個屬性一起用」的組合做。
檢查清單
| 項目 | 標準 |
|---|---|
每個 @font-face 都有 font-display | 必備 |
值是 swap 或 optional,不是 auto 或 block | 必備 |
預載的字型有 crossorigin | 必備 |
| 只預載一個字型檔 | 必備 |
第三方字型服務的網址帶 display=swap | 必備 |
| 有處理備援字型的度量落差 | 建議 |
用 preconnect 提早連線(第三方字型) | 建議 |
| 沒有用 JavaScript 控制字型顯示 | 建議 |
| 用 Layout Shift Regions 驗證過換字不位移 | 建議 |
常見問題
五個值該選哪一個?
中文網站預設選 swap,它保證文字立刻顯示,只是之後會換字。極度在意 版面穩定度 的話選 optional,它完全不會有換字造成的位移,代價是首次造訪可能看不到品牌字型。
為什麼不建議用 block?
它在字型載入完成前完全不顯示文字。英文字型可能只是一瞬間,但中文字型動輒數 MB,那段空白可能長達數秒,使用者面對的是一片空白的頁面,載入速度指標 也會一併被算進去。
預載字型一定要加 crossorigin 嗎?
一定要,即使是同網域的字型。字型的請求一律以匿名跨來源模式發出,預載時不加這個屬性會讓瀏覽器認為是兩個不同的請求而下載兩次,完全失去預載的意義。
要預載幾個字型檔?
只預載首屏一定會用到的那一個,通常是正文的常規字重。預載了四個字重等於讓它們互相搶頻寬,反而拖慢載入速度指標。標題用的粗體字重通常不必預載。
怎麼減少換字時的版面跳動?
用 size-adjust 這類度量覆寫屬性讓備援字型的字寬與行高接近目標字型。宣告一個以系統字型為來源的備援字族,調整它的縮放比例與上下高度,再把它放在字族清單裡目標字型的後面。
延伸閱讀
參考資料:web.dev:font-display