最佳化 WebFont 載入和算繪

Ilya Grigorik
Ilya Grigorik

完整的 WebFont 包含所有風格變化 (您可能不需要),以及所有可能未使用的字形,因此很容易導致下載檔案大小達到數 MB。本文將說明如何最佳化 Web Fonts 的載入作業,讓訪客只下載所需的字型。

為解決包含所有變體的大檔案問題,@font-face CSS 規則是專門設計用來將字型系列拆分為資源集合。例如:萬國碼子集和不同的樣式變體。

有了這些宣告,瀏覽器就能找出所需的子集和變體,並下載轉譯文字所需的最小集合,非常方便。不過,如果您不小心,也會在關鍵算繪路徑中造成效能瓶頸,並延遲文字算繪作業。

延後載入字型會帶來一個重要的隱含影響,可能會延遲文字算繪作業。瀏覽器必須先建構轉譯樹狀結構 (取決於 DOM 和 CSSOM 樹狀結構),才能知道需要哪些字型資源來轉譯文字。因此,字型要求會在其他重要資源之後延遲,瀏覽器可能會在擷取資源前,就停止轉譯文字。

字型關鍵算繪路徑

  1. 瀏覽器要求 HTML 文件。
  2. 瀏覽器開始剖析 HTML 回應並建構 DOM。
  3. 瀏覽器會找出 CSS、JS 和其他資源,並調度要求。
  4. 瀏覽器會在收到所有 CSS 內容後建構 CSSOM,並將其與 DOM 樹狀結構結合,以建構算繪樹狀結構。
    • 轉譯樹狀結構指出需要哪些字型變化才能在網頁上轉譯指定文字後,系統就會調度字型要求。
  5. 瀏覽器會執行版面配置,並將內容繪製至螢幕。
    • 如果尚未提供字型,瀏覽器可能不會轉譯任何文字像素。
    • 字型可供使用後,瀏覽器就會繪製文字像素。

網頁內容的首次著色作業之間的「競爭」,可在轉譯樹建構後不久完成,而字型資源的要求會造成「空白文字問題」,在這種情況下,瀏覽器可能會轉譯網頁版面配置,但省略任何文字。

預先載入 Web 字型,並使用 font-display 控制瀏覽器在無法使用的字型下如何運作,即可避免因字型載入而導致空白頁面和版面配置位移。

預先載入 WebFont 資源

如果您的網頁很可能需要在您事先知道的網址中代管特定 WebFont,您可以利用資源優先順序。使用 <link rel="preload"> 會在關鍵算繪路徑的早期觸發對 WebFont 的請求,不必等待 CSSOM 建立。

自訂文字轉譯延遲時間

雖然預先載入可讓網頁內容顯示時更有可能使用 WebFont,但無法保證一定可行。您仍需考量瀏覽器在轉譯使用尚未提供的 font-family 的文字時的行為。

在「避免在字型載入期間顯示不可見的文字」一文中,您可以看到預設瀏覽器的行為不一致。不過,您可以使用 font-display,告知新式瀏覽器您希望的行為。

瀏覽器支援

  • Chrome:60 分鐘。
  • Edge:79。
  • Firefox:58。
  • Safari:11.1。

資料來源

與部分瀏覽器實作的現有字型逾時行為類似,font-display 將字型下載的生命週期分為三個主要期間:

  1. 第一個逗號是字型區塊的逗號。在此期間,如果未載入字型面,則任何嘗試使用該字型的元素,都必須以不可見的備用字型面顯示。如果字型面在封鎖期間成功載入,系統就會正常使用字型面。
  2. 字型換換期會在字型封鎖期結束後立即開始。在此期間,如果未載入字型面,則任何嘗試使用該字型的元素都必須改為使用備用字型面進行轉譯。如果字型在切換期間成功載入,系統就會正常使用該字型。
  3. 字型換換期間結束後,系統會立即進入字型失敗期間。如果字型面在這個期間開始時尚未載入,系統會將其標示為載入失敗,導致正常的字型備用機制。否則,系統會正常使用字型。

瞭解這些時間長度後,您就可以使用 font-display 決定字型應如何顯示,這取決於字型是否已下載,以及下載時間。

如要使用 font-display 屬性,請將其新增至 @font-face 規則:

@font-face {
  font-family: 'Awesome Font';
  font-style: normal;
  font-weight: 400;
  font-display: auto; /* or block, swap, fallback, optional */
  src: local('Awesome Font'),
       url('/fonts/awesome-l.woff2') format('woff2'), /* will be preloaded */
       url('/fonts/awesome-l.woff') format('woff'),
       url('/fonts/awesome-l.ttf') format('truetype'),
       url('/fonts/awesome-l.eot') format('embedded-opentype');
  unicode-range: U+000-5FF; /* Latin glyphs */
}

font-display 目前支援下列值範圍:

  • auto
  • block
  • swap
  • fallback
  • optional

如要進一步瞭解如何預先載入字型和 font-display 屬性,請參閱下列文章:

字型載入 API

<link rel="preload"> 和 CSS font-display 搭配使用時,可讓您大幅控管字型載入和算繪作業,且不會增加太多額外負擔。不過,如果您需要額外自訂功能,且願意承擔執行 JavaScript 所帶來的額外負擔,還有另一個選項。

字型載入 API 提供指令碼介面,可定義及操作 CSS 字型面,追蹤下載進度,並覆寫預設的延遲載入行為。舉例來說,如果您確定需要特定字型變化版本,可以定義該版本,並告知瀏覽器立即擷取字型資源:

瀏覽器支援

  • Chrome:35。
  • Edge:79。
  • Firefox:41。
  • Safari:10。

資料來源

var font = new FontFace("Awesome Font", "url(/fonts/awesome.woff2)", {
  style: 'normal', unicodeRange: 'U+000-5FF', weight: '400'
});

// don't wait for the render tree, initiate an immediate fetch!
font.load().then(function() {
  // apply the font (which may re-render text and cause a page reflow)
  // after the font has finished downloading
  document.fonts.add(font);
  document.body.style.fontFamily = "Awesome Font, serif";

  // OR... by default the content is hidden,
  // and it's rendered after the font is available
  var content = document.getElementById("content");
  content.style.visibility = "visible";

  // OR... apply your own render strategy here...
});

此外,由於您可以透過 check() 方法查看字型狀態並追蹤下載進度,因此也可以定義在網頁上顯示文字的自訂策略:

  • 您可以暫停所有文字轉譯作業,直到字型可供使用為止。
  • 您可以為每個字型實作自訂逾時時間。
  • 您可以使用備用字型來解除封鎖的轉譯作業,並在字型可用後插入使用所需字型的新樣式。

更棒的是,您還可以根據網頁上的不同內容,搭配使用上述策略。舉例來說,您可以延遲部分區段的文字算繪作業,直到字型可用為止,然後使用備用字型,並在字型下載完成後重新算繪。

必須妥善快取

字型資源通常是不會經常更新的靜態資源。因此,這些資源非常適合用於長時間到期。請務必為所有字型資源指定條件式 ETag 標頭最佳 Cache-Control 政策

如果您的網頁應用程式使用服務工作者,則在大多數用途下,使用快取優先策略提供字型資源都相當合適。

請勿使用 localStorageIndexedDB 儲存字型,因為這兩者都存在各自的效能問題。瀏覽器的 HTTP 快取提供最佳且最可靠的機制,可將字型資源提供給瀏覽器。

WebFont 載入檢查清單

  • 使用 <link rel="preload">font-display 或字型載入 API 自訂字型載入和轉譯:預設的延遲載入行為可能會導致文字轉譯延遲。這些網路平台功能可讓您針對特定字型覆寫這項行為,並為網頁上的不同內容指定自訂轉譯和逾時策略。
  • 指定重新驗證和最佳快取政策:字型是靜態資源,更新頻率不高。請確認您的伺服器提供長效的最大年齡時間戳記和重新驗證權杖,以便在不同網頁之間有效重複使用字型。如果使用服務工作站,則適合採用快取優先策略。

使用 Lighthouse 自動測試 WebFont 載入行為

Lighthouse 可協助自動化這項程序,確保您遵循網路字型最佳化最佳做法。

您可以透過下列稽核作業,確保網頁持續遵循網路字型最佳化最佳做法: