Font Loading APICLSfont-display前端效能Web 效能2026

Font Loading API:零 CLS 的字體效能優化

Font Loading API 完整教學!72% 字體失敗原因、零 CLS 教學、font-display 策略攻略。2026 年工程師必看的 Web 效能優化指南!

· 6 分鐘閱讀

如果你的網站在文字還沒載入時顯示的是 Arial,載入後突然變成了 Noto Sans CJK——那不只是「醜」的問題,這是 CLS(Cumulative Layout Shift,累積版面位移)在作祟。CLS 是 Google Core Web Vitals 之一,會直接影響搜尋排名。2026 年的 HTTP Archive 數據顯示,72% 的字體相關失敗來自 Swap Timing——也就是字體下載完成後、置換的那一刻發生的版面位移。Font Loading API 提供了程式化的控制能力,讓你可以在字體載入的不同階段做精確的處理。


為什麼字體會造成版面位移

font-display 的預設行為

當瀏覽器遇到 <link rel="stylesheet"> 載入字體時,有一個「等待期」(block period):

等待期(block period):
- 如果字體還沒下載完成,瀏覽器有兩個選擇:
  - 等待(block)
  - 馬上用 fallback 顯示(swap)

font-display 屬性決定了這個行為:

@font-face {
  font-family: 'NotoSansCJK';
  src: url('/fonts/NotoSansCJK.woff2') format('woff2');
  font-display: swap;  /* 用 fallback 顯示,等下載完再置換 */
}

swap 的問題

font-display: swap 是多數開發者推薦的策略——讓用戶盡快看到文字內容。但置換的那一刻,如果 fallback 字體和目標字體的 metrics(高、寬、間距)差異大,就會造成 CL:

/* fallback 字體(Arial) */
/* 字重不同、高度不同、字符寬度不同 */

/* 載入後置換成 Noto Sans CJK */
/* 整個版面突然跳了一下——這就是 CLS */

/* 72% 的字體失敗發生在這裡:swap timing */

Font Loading API:程式化控制

Font Loading API 讓你用 JavaScript 控制字體的載入過程。

FontFace:定義新字體

// 建立一個 FontFace 物件
const notoSans = new FontFace(
  'NotoSansCJK',
  'url(/fonts/NotoSansCJK-Regular.woff2) format("woff2")',
  {
    style: 'normal',
    weight: '400',
  }
);

// 註冊到 document.fonts
document.fonts.add(notoSans);

// 開始下載
notoSans.load();

document.fonts.load():主動下載

// 主動下載字體,下載完成後執行回調
document.fonts.load('16px "NotoSansCJK"').then((loadedFonts) => {
  console.log(`載入了 ${loadedFonts.length} 個字體`);
  // 可以這裡加 class 或執行其他邏輯
});

監聽載入狀態

// 監聽特定字體的載入完成
document.fonts.addEventListener('loadingdone', (event) => {
  event.fontfaces.forEach((fontFace) => {
    if (fontFace.family === 'NotoSansCJK') {
      console.log('NotoSansCJK 載入完成');
      document.documentElement.classList.add('fonts-loaded');
    }
  });
});

// 監聽所有字體載入完成
document.fonts.ready.then(() => {
  console.log('所有字體載入完成');
});

零 CLS 的實作策略

策略 1:Fallback Metrics Matching

讓 fallback 字體的 metrics 與目標字體盡可能接近:

@font-face {
  font-family: 'NotoSansCJK-Fallback';
  src: local('Arial');  /* 使用系統字體作為 fallback */
  /* 透過 ascent-override、descent-override 調整 metrics */
  ascent-override: 90%;
  descent-override: 25%;
  line-gap-override: 0%;
  size-adjust: 98%;
}

@font-face {
  font-family: 'NotoSansCJK';
  src: url('/fonts/NotoSansCJK-Regular.woff2') format('woff2');
  font-display: swap;
  /* 為 fallback 版本定義同樣的 override */
  ascent-override: 90%;
  descent-override: 25%;
  line-gap-override: 0%;
  size-adjust: 98%;
}

這些 *-override CSS 屬性讓你調整 fallback 字體的 metrics,使其與目標字體接近。工具如 Fontaine 可以自動生成這些值。

策略 2:程式化載入 + Class 控制

// 當關鍵字體載入完成後,才顯示對應文字
document.fonts.load('bold 700 16px "NotoSansCJK"').then(() => {
  document.documentElement.classList.add('fonts-ready');
});
/* 預設:使用系統字體,避免下載前的 FOUT */
body {
  font-family: system-ui, -apple-system, sans-serif;
}

/* 當字體載入完成後,才切換到 Noto Sans CJK */
.fonts-ready body {
  font-family: 'NotoSansCJK', system-ui, -apple-system, sans-serif;
}

這個策略完全消滅了 swap 帶來的 CLS——因為文字一開始就使用正確的字體渲染,只不過是系統字體。字體下載完成後,瀏覽器才切換到 Noto Sans CJK,metrics 完全一致所以不會有位移。

策略 3:Progressive Loading + Unicode Range

只下載需要的字符,減少下載量:

<!-- 只下載 Latin 和基本標點符號 -->
<link rel="preload" href="/fonts/noto-sans-latin.woff2" as="font" type="font/woff2" crossorigin>

<!-- CJK 延後載入 -->
<script>
  // 當用戶滾動到 CJK 內容區時,再載入 CJK 字體
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        loadCJKFonts();
        observer.disconnect();
      }
    });
  });

  observer.observe(document.querySelector('.cjk-content'));
</script>
/* Unicode range:只下載需要的字符 */
@font-face {
  font-family: 'NotoSansCJK';
  src: url('/fonts/noto-sans-cjk-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153;
  /* Latin 字符使用這個 */
}

策略 4:Preload 關鍵字體

<!-- 預先下載最重要的字體 -->
<link rel="preload" href="/fonts/noto-sans-latin-400.woff2" as="font" type="font/woff2" crossorigin>

<!-- 讓瀏覽器盡快開始下載 -->
<link rel="preconnect" href="https://fonts.example.com" crossorigin>

font-display 各策略詳解

策略等待期行為置換行為CLS 風險
block隱藏文字(最多 3s)無置換
swap顯示 fallback下載後置換
fallback極短隱藏(~100ms)不置換
optional網路決定不置換最低
/* 根據字體重要性選擇 */
@font-face {
  /* 重要標題:swap,但做好 metrics matching */
  font-family: 'Brand-Font';
  src: url('/fonts/brand-bold.woff2') format('woff2');
  font-display: swap;
  ascent-override: 95%;
  descent-override: 25%;
}

@font-face {
  /* 次要裝飾字體:optional,讓瀏覽器自行決定 */
  font-family: 'Decorative';
  src: url('/fonts/decorative.woff2') format('woff2');
  font-display: optional;
}

72% Swap Timing 失敗的解決方案

HTTP Archive 2025 的數據顯示,72% 的字體失敗發生在 swap timing——字體下載完成後、置換的那一刻。這個失敗的根源是 fallback 字體和目標字體的 metrics 差異過大

快速檢測

// 測量字體置換帶來的 CLS
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.sources) {
      entry.sources.forEach((source) => {
        if (source.node && source.node.style) {
          console.log('Layout shift:', entry.value, source.node);
        }
      });
    }
  }
});

observer.observe({ type: 'layout-shift', buffered: true });

優先解決高影響的字體

// 找出哪些字體造成最大的版面位移
// 這些是需要優先優化的

實際工作流

Step 1:識別需要優化的字體

// 列出所有載入的字體
for (const fontFace of document.fonts) {
  console.log({
    family: fontFace.family,
    weight: fontFace.weight,
    style: fontFace.style,
    status: fontFace.status,
  });
}

Step 2:分類字體策略

// 關鍵字體(LOGO、標題):swap + metrics matching
// 正文字體:swap + metrics matching
// 裝飾/Icon 字體:optional

Step 3:實施優化

// 1. 預先載入關鍵字體
const criticalFonts = ['/fonts/heading.woff2', '/fonts/body.woff2'];
criticalFonts.forEach((url) => {
  const font = new FontFace('Critical', `url(${url}) format("woff2")`);
  document.fonts.add(font);
  font.load();
});

// 2. 延後載入非關鍵字體
document.fonts.ready.then(() => {
  loadSecondaryFonts();
});

// 3. 監控並修正 CLS

結語:字體效能是被低估的優化點

字體效能優化在多數前端效能工作裡優先級不高,但它對 CLS 的影響是直接而且可測量的。72% 的字體失敗來自 swap timing,這意味著多數的字體 CLS 問題可以透過 fallback metrics matching 解決

Font Loading API 給了你程式化的控制能力——你可以決定什麼時候下載、什麼時候置換、置換時做什麼處理。配合 font-display 策略和 ascent-override 等 CSS 屬性,你可以做到真正的零 CLS 字體載入。


延伸閱讀

本文是「2026 Web 效能優化」系列文章之一。