Core Web Vitals 效能優化專家
建立摘要
LCP 優化 LCP CLS INP

font-display 對 LCP 的影響:FOIT 機制、五值對比與實測數據

font-display 五個值(block/swap/fallback/optional/auto)對 LCP、FCP、CLS 的具體影響,含 3G 實測數據、swap CLS 修正方案(size-adjust)、preload 搭配策略與 DevTools 診斷流程。

陳

陳柏翰

網站效能工程師

3 分鐘閱讀
font-display 對 LCP 的影響:FOIT 機制、五值對比與實測數據

我拿了一個 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 的影響比較圖,包含 FOIT、FOUT 時間軸說明
font-display 五個值與 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 還快。

font-display block、swap、fallback、optional 五個值的 FOIT FOUT 時間軸對比視覺圖,標示 font block period 和 swap period
font-display 各值的 block period 與 swap period 時間軸比較

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 為例,修正流程如下:

  1. 先用 DebugBear Font Display Analyzer 或 document.fonts API 取得 webfont 的 UPM(units per em)和 metrics
  2. 計算 size-adjust:Montserrat 的 xAvgCharWidth / Arial 的 xAvgCharWidth,約 0.94
  3. 補 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 面板可以直接看到字體的下載時間和阻塞情況。

步驟:

  1. 打開 DevTools(F12)→ Network → 在 Filter 欄輸入 font,只顯示字體請求
  2. 看每個字體檔的 Waterfall:藍色條是 DNS + TCP + TLS,橘色是 TTFB,綠色是 Content Download
  3. 在 Network 面板上方勾選 Slow 3G,重新 Reload,看字體下載完成時間落在哪個時間點
  4. 切換到 Performance 面板 → 錄製 → 找 Layout 事件,看有沒有在字體下載完成後緊接著一個大 Layout,那通常就是 FOUT 觸發的 CLS
  5. 用 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 的機制是讓文字元素的「出現時機」延遲;渲染阻塞則是讓整個渲染管線停下來等資源。兩個同時存在時,要先確認誰是主要瓶頸,再決定修哪個。關於 瀏覽器渲染機制的理論基礎,可以回頭參考那篇文章。

陳

陳柏翰

網站效能工程師

專注前端效能優化超過 8 年,曾協助電商、媒體和 SaaS 平台改善 Core Web Vitals 指標。相信效能優化應該基於數據而非直覺。

50+ 效能實驗
8 年 前端經驗
200+ 優化案例

想看更多?

探索所有效能實驗

查看完整的實驗報告列表,找到你需要的效能優化方案。

所有實驗報告 免費效能診斷