我拿了一個 Next.js 專案測試 Google Font(Montserrat),在 WebPageTest 3G 模擬下跑了四個版本:不設 font-display(預設 block 行為)、swap、swap 加 size-adjust、optional。LCP 從 3.2s 壓到 1.9s,但 CLS 從 0.02 跳到 0.22,然後再修回 0.06。這篇把那個過程和數據整理出來,也說明什麼情境下哪個 font-display 值對 LCP 最有利。
font-display 的 FOIT 如何讓 LCP 遲遲不動
FOIT(Flash of Invisible Text)是 LCP 延遲最容易被忽略的原因:瀏覽器在 font block period 內不渲染文字,導致 LCP 計算被延後到字體下載完成的那一刻。根據 web.dev LCP 規格說明,「使用 web font 且處於 font block period 的文字節點不被視為已渲染」,這意味著即使 DOM 已經完成、HTML 已經解析,Largest Contentful Paint 也要等字體就位才觸發。
font block period 的長短由 font-display 的值決定。預設值 auto 在 Chrome 和 Firefox 上的行為接近 block,會有 2-3 秒的隱形等待期。這段時間內,如果 LCP 的候選元素是一個 <h1> 或大段文字,這個元素就是「存在但看不到」,LCP 計時器一直在跑,等到字體載入完成、文字出現,那個時間點才是最終 LCP。
實際的瓶頸在哪裡?把 Google Fonts embed code 放進 <head> 的網站,字體請求本身就是跨域的:DNS 查詢 → TCP 連線 → TLS 握手 → 下載,在 3G 模擬下這一段就要 600-900ms。加上 font block period,LCP 文字元素的出現時間往往比圖片慢一大截。
五種 font-display 值對 LCP 的實際影響對比
以下是同一個 Next.js 頁面在 WebPageTest 3G 模擬下,換不同 font-display 值後的實測結果。LCP 元素是 <h1>,使用 Google Font Montserrat。
| font-display 值 | LCP | FCP | CLS | 行為說明 |
|---|---|---|---|---|
auto(預設) |
3.2s | 2.9s | 0.02 | 約 3s 隱形期,等字體才顯示文字(FOIT),LCP 最慢 |
block |
3.1s | 2.8s | 0.01 | 明確 3s 隱形期,字體到前不顯示,無 FOUT;LCP 依然很慢 |
swap |
1.9s | 1.4s | 0.22 | 立即顯示 fallback 字體,LCP 提前觸發;字體到後替換觸發 CLS |
swap + size-adjust |
1.9s | 1.4s | 0.06 | LCP 同 swap,CLS 因 fallback 字體調整而大幅降低 |
fallback |
2.1s | 1.6s | 0.08 | 100ms 隱形 + 3s swap window,字體太晚到就放棄換,CLS 風險中等 |
optional |
2.0s | 1.5s | 0.01 | 100ms 隱形,之後不換字;首次訪問用 fallback,回訪才用 webfont;CLS 最低 |
幾個數字值得注意。swap 的 LCP 改善最明顯,從 3.2s 降到 1.9s,但 CLS 從 0.02 跳到 0.22,超過 Google 的 0.1 閾值。optional 的 LCP 比 swap 慢一點點(2.0s vs 1.9s),但 CLS 只有 0.01,而且這是第一次訪問的數字;回訪用戶因為字體已快取,LCP 通常比 swap 還快。
LCP 元素是文字時,要選哪個 font-display?
答案取決於三個因素:LCP 元素是否為文字、品牌字體對設計的重要性、目標用戶的網速分佈。下面是一個決策框架,根據這三個維度選出最合適的值。
| 情境 | 推薦值 | 原因 |
|---|---|---|
| LCP 是文字,用戶多為 4G/WiFi | swap + size-adjust |
LCP 最快,CLS 能靠 size-adjust 壓到 0.1 以下 |
| LCP 是文字,用戶多為 3G 或慢速網路 | optional + preload |
preload 讓字體趕上 100ms 窗口;萬一趕不上,fallback 保持版面穩定,回訪就有 webfont |
| LCP 是圖片,字體是品牌關鍵元素 | fallback 或 swap |
LCP 不受字體影響,可接受輕微 FOUT;fallback 的 3s swap window 給字體足夠時間 |
| Icon font(Material Icons 等) | block |
icon font 的 fallback 字符無意義,寧可隱形等待也不要顯示亂碼 |
| 字體對設計無關緊要(body text) | optional |
CLS 最低,不強迫用戶下載非關鍵字體 |
關於 optional 一個常見的誤解:很多人以為 optional 第一次訪問就看不到 webfont,所以沒用。但如果同時加上 <link rel="preload">,字體可以在 100ms 窗口內完成下載(在快速網路 + CDN 的情況下),第一次訪問就能用 webfont,且完全沒有 CLS。這個組合在 Chrome 官方字體最佳實踐裡也有說明,但搭配 optional 的策略在中文資源裡很少完整解釋。
swap 的副作用:字體替換觸發的 CLS 怎麼解
用 font-display: swap 最常見的問題是 CLS:瀏覽器先用 system font(例如 Arial 或 Helvetica)顯示文字,webfont 下載完後替換,因為兩者的字重、行高、字距不同,版面重新排列,CLS 就出現了。解法是用 CSS font metric overrides 讓 fallback 字體在視覺上盡量接近 webfont。
以 Montserrat 配 Arial 作為 fallback 為例,修正流程如下:
- 先用 DebugBear Font Display Analyzer 或
document.fontsAPI 取得 webfont 的 UPM(units per em)和 metrics - 計算
size-adjust:Montserrat 的 xAvgCharWidth / Arial 的 xAvgCharWidth,約 0.94 - 補
ascent-override和descent-override讓行高對齊
/* 調整後的 fallback font,盡量符合 Montserrat 的視覺比例 */
@font-face {
font-family: "Montserrat Fallback";
src: local("Arial");
size-adjust: 94%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: "Montserrat", "Montserrat Fallback", Arial, sans-serif;
}
這個方案把 CLS 從 0.22 壓到 0.06(對齊前後的數字來自上面的實測表)。要完全壓到 0 很難,因為 Arial 和 Montserrat 的字形細節還是有差異,但 0.06 已遠低於 Google 的 0.1 閾值,CWV 判定為 Good。
注意:Safari 在 macOS 上預設 system font 是 San Francisco,不是 Arial,所以這個 fallback 設定在 Safari 上的 CLS 修正效果會略差。但要針對 Safari 再做一套 fallback 的成本偏高,如果目標用戶以 Windows Chrome 為主,上面的設定已足夠。
preload + font-display 的組合拳:讓文字 LCP 提前觸發
光設 font-display: swap 只是「字體載入期間顯示 fallback」,並不縮短字體本身的下載時間。要讓 LCP 文字元素更快切換到 webfont,需要搭配 <link rel="preload"> 告訴瀏覽器提早開始下載。
標準寫法:
<!-- 在 <head> 中,越早越好 -->
<link rel="preload" href="/fonts/montserrat-v26-latin-regular.woff2"
as="font" type="font/woff2" crossorigin="anonymous">
<!-- @font-face 中同步設定 -->
<style>
@font-face {
font-family: 'Montserrat';
src: url('/fonts/montserrat-v26-latin-regular.woff2') format('woff2');
font-display: swap;
}
</style>
幾個細節容易出錯:
crossorigin="anonymous"是必要的。瀏覽器處理字體請求預設帶 CORS mode,如果 preload 沒有設 crossorigin,瀏覽器會發兩次請求(一次 preload、一次實際使用),preload 等於沒用。- 只 preload 你真正用到的字重/字型。如果 preload 了 Regular 和 Bold 兩個檔案,但 LCP 元素只用 Regular,Bold 的 preload 佔用了早期下載頻寬,反而可能拖慢 LCP。
- 自架字體的 preload 效果最穩定。Google Fonts 的 preload 需要 preconnect 到
fonts.gstatic.com,且字體 URL 可能隨 Google 的 CDN 路徑變動,字體更新後 preload URL 會失效。
跟 fetchpriority 優化圖片 LCP 的邏輯類似:不是只靠 font-display 的顯示策略,而是從資源下載的優先順序直接縮短延遲。如果 LCP 元素是圖片,用 fetchpriority;如果是文字,用 preload + font-display。
如何用 DevTools 和 Lighthouse 確認字體影響 LCP
在動手改 font-display 之前,要先確認字體是不是 LCP 延遲的真正原因。用 Chrome DevTools 的 Network 面板可以直接看到字體的下載時間和阻塞情況。
步驟:
- 打開 DevTools(F12)→ Network → 在 Filter 欄輸入
font,只顯示字體請求 - 看每個字體檔的 Waterfall:藍色條是 DNS + TCP + TLS,橘色是 TTFB,綠色是 Content Download
- 在 Network 面板上方勾選 Slow 3G,重新 Reload,看字體下載完成時間落在哪個時間點
- 切換到 Performance 面板 → 錄製 → 找 Layout 事件,看有沒有在字體下載完成後緊接著一個大 Layout,那通常就是 FOUT 觸發的 CLS
- 用 LCP badge(Performance 面板裡的 Timings 欄位)確認 LCP 元素的標記時間,對比字體下載完成時間,兩者接近代表字體就是 LCP 瓶頸
Lighthouse 方面,「Ensure text remains visible during webfont load」這個 audit 會標記所有沒設 font-display(或設成 block/auto)的字體。但 Lighthouse 不會告訴你字體對 LCP 的影響有多大,那個還是要用 Performance 面板的 LCP marking 來確認。
關於 LCP 整體優化的其他面向,字體只是其中一個因素。如果 Lighthouse 給了 90 分,但 CrUX field data 的 LCP 還是偏慢,通常是字體延遲或 TTFB 的問題,而不是 lab data 看不到的部分。
Google Fonts 和自架字體的 font-display 設定差異
Google Fonts 自 2020 年起在 embed URL 加入 &display=swap 參數,等同於替所有字體設定 font-display: swap。如果你的 Google Fonts embed 沒有這個參數,Lighthouse 的 Font Display audit 會標記它。修正方式:
<!-- 有 display=swap 的正確寫法 -->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap"
rel="stylesheet">
但 Google Fonts 的限制是你無法指定 font-display: optional 或 font-display: fallback,URL 參數只支援 swap、block、fallback、optional,而 Google 的 CSS 預設插入的就是 swap,你沒辦法直接在 <link> 裡細調。想要完整控制,需要自架字體。
自架字體的 @font-face 設定完整範例:
@font-face {
font-family: 'Montserrat';
src: url('/fonts/montserrat-regular.woff2') format('woff2'),
url('/fonts/montserrat-regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: optional; /* 或 swap,依上面決策框架選擇 */
}
自架字體的另一個好處是省去跨域 DNS + TCP 查詢。Google Fonts 需要 preconnect 到 fonts.googleapis.com 和 fonts.gstatic.com 兩個域名;自架的話字體跟頁面同域,TCP 連線已經建立好了,節省 100-300ms(3G 網路下更明顯)。
整體而言,這個問題的瓶頸分析跟 渲染阻塞資源的消除方式 有相通之處:都是在搶「first render」的時間視窗。字體影響 LCP 的機制是讓文字元素的「出現時機」延遲;渲染阻塞則是讓整個渲染管線停下來等資源。兩個同時存在時,要先確認誰是主要瓶頸,再決定修哪個。關於 瀏覽器渲染機制的理論基礎,可以回頭參考那篇文章。