最佳化資源載入作業

在上一個單元中,我們探討了重要算繪路徑背後的一些理論,以及會阻斷算繪和剖析的資源如何延遲網頁的初始算繪。現在您已瞭解這項技術背後的部分理論,接下來要學習一些最佳化重要算繪路徑的技巧。

網頁載入時,HTML 中會參照許多資源,透過 CSS 提供網頁的外觀和版面配置,並透過 JavaScript 提供互動功能。本單元將介紹與這些資源相關的許多重要概念,以及這些資源如何影響網頁載入時間。

轉譯封鎖

前一個單元所述,CSS 是會阻斷轉譯的資源,因為在建構 CSS 物件模型 (CSSOM) 之前,瀏覽器會阻斷任何內容的轉譯作業。瀏覽器會封鎖算繪作業,避免發生未套用樣式的內容閃爍 (FOUC),從使用者體驗的角度來看,這是不可取的行為。

在先前的影片中,您會看到短暫的 FOUC,也就是沒有任何樣式的網頁。隨後,網頁的 CSS 從網路載入完畢後,所有樣式都會套用,且網頁的未套用樣式版本會立即替換為套用樣式版本。

一般來說,您通常不會看到 FOUC,但瞭解這個概念很重要,這樣您就會知道為什麼瀏覽器會封鎖網頁的算繪作業,直到 CSS 下載完畢並套用至網頁為止。算繪封鎖不一定是不良現象,但您應盡量縮短封鎖時間,方法是持續最佳化 CSS。

剖析器封鎖

剖析器封鎖資源會中斷 HTML 剖析器,例如沒有 asyncdefer 屬性的 <script> 元素。當剖析器遇到 <script> 元素時,瀏覽器必須先評估及執行指令碼,才能繼續剖析其餘的 HTML。這是刻意設計的行為,因為指令碼可能會在 DOM 建構期間修改或存取 DOM。

<!-- This is a parser-blocking script: -->
<script src="/script.js"></script>

使用外部 JavaScript 檔案 (不含 asyncdefer) 時,剖析器會遭到封鎖,直到檔案下載、剖析及執行完畢為止。使用行內 JavaScript 時,剖析器同樣會遭到封鎖,直到剖析並執行行內指令碼為止。

預先載入掃描器

預先載入掃描器是一種瀏覽器最佳化功能,以次要 HTML 剖析器的形式存在,可掃描原始 HTML 回應,在主要 HTML 剖析器發現資源之前,找出並推測性地擷取資源。舉例來說,即使 HTML 剖析器在擷取及處理 CSS 和 JavaScript 等資源時遭到封鎖,預載掃描器仍可讓瀏覽器開始下載 <img> 元素中指定的資源。

如要善用預先載入掃描器,請在伺服器傳送的 HTML 標記中加入重要資源。預先載入掃描器無法探索下列資源載入模式:

  • CSS 使用 background-image 屬性載入的圖片。這些圖片參照位於 CSS 中,預先載入掃描器無法探索。
  • <script> 元素標記形式動態載入的指令碼,會使用 JavaScript 或透過動態 import() 載入的模組,插入 DOM 中。
  • 使用 JavaScript 在用戶端顯示的 HTML。這類標記會包含在 JavaScript 資源的字串中,預先載入掃描器無法偵測到。
  • CSS @import 宣告。

這些資源載入模式都是稍後才發現的資源,因此無法從預先載入掃描器獲益。請盡量避免使用。不過,如果無法避免這類模式,或許可以使用 preload 提示,避免資源探索延遲。

CSS

CSS 會決定網頁的呈現方式和版面配置。如先前所述,CSS 是會阻斷轉譯的資源,因此最佳化 CSS 可能會對整體網頁載入時間造成顯著影響。

壓縮

壓縮 CSS 檔案可縮減 CSS 資源的檔案大小,加快下載速度。主要做法是從來源 CSS 檔案中移除內容,例如空格和其他不可見字元,然後將結果輸出至新最佳化的檔案:

/* Unminified CSS: */

/* Heading 1 */
h1 {
  font-size: 2em;
  color: #000000;
}

/* Heading 2 */
h2 {
  font-size: 1.5em;
  color: #000000;
}
/* Minified CSS: */
h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}

最基本的 CSS 壓縮是有效的最佳化方式,可改善網站的首次內容繪製 (FCP),在某些情況下甚至能改善最大內容繪製 (LCP)。組合器等工具可在正式版建構作業中,自動為您執行這項最佳化作業。

移除未使用的 CSS

瀏覽器必須先下載並剖析所有樣式表,才能算繪任何內容。完成剖析所需的時間也包括目前網頁上未使用的樣式。如果您使用的打包工具會將所有 CSS 資源合併為單一檔案,使用者可能會下載超出目前網頁顯示需求量的 CSS。

如要找出目前網頁未使用的 CSS,請使用 Chrome 開發人員工具中的涵蓋率工具

Chrome 開發人員工具中涵蓋範圍工具的螢幕截圖。在底部窗格中選取 CSS 檔案,顯示目前頁面版面配置未使用的 CSS 數量相當多。
Chrome 開發人員工具中的涵蓋率工具,可偵測目前網頁未使用的 CSS (和 JavaScript)。可用於將 CSS 檔案分割成多個資源,由不同網頁載入,而非傳送可能延遲網頁算繪作業的較大 CSS 組合。

移除未使用的 CSS 有雙重效果:除了縮短下載時間,您還能最佳化算繪樹狀結構的建構作業,因為瀏覽器需要處理的 CSS 規則較少。

避免使用 CSS @import 宣告

雖然這似乎很方便,但您應避免在 CSS 中使用 @import 宣告:

/* Don't do this: */
@import url('style.css');

與 HTML 中的 <link> 元素運作方式類似,CSS 中的 @import 宣告可讓您從樣式表匯入外部 CSS 資源。這兩種做法的主要差異在於,HTML <link> 元素是 HTML 回應的一部分,因此比 @import 宣告下載的 CSS 檔案更早被發現。

這是因為系統必須下載含有 @import 宣告的 CSS 檔案,才能探索該宣告。這會導致所謂的要求鏈,以 CSS 來說,這會延遲網頁的初始轉譯時間。另一個缺點是,使用 @import 宣告載入的樣式表無法由預先載入掃描器探索,因此會成為延遲探索的阻礙算繪資源。

<!-- Do this instead: -->
<link rel="stylesheet" href="style.css">

在大多數情況下,您可以使用 <link rel="stylesheet"> 元素取代 @import<link> 元素可同時下載樣式表,並縮短整體載入時間,而 @import 宣告則會依序下載樣式表。

內嵌重要 CSS

下載 CSS 檔案的時間越長,網頁的 FCP 就會越慢。在文件 <head> 中內嵌重要樣式,可避免對 CSS 資源發出網路要求,而且如果做法正確,當使用者瀏覽器的快取未預先載入時,可以縮短初始載入時間。其餘 CSS 可以非同步載入,或附加在 <body> 元素的結尾。

<head>
  <title>Page Title</title>
  <!-- ... -->
  <style>h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}</style>
</head>
<body>
  <!-- Other page markup... -->
  <link rel="stylesheet" href="non-critical.css">
</body>

但缺點是,內嵌大量 CSS 會在初始 HTML 回應中新增更多位元組。由於 HTML 資源通常無法長時間 (或完全無法) 快取,這表示內嵌 CSS 不會快取,後續網頁可能使用外部樣式表中的相同 CSS。測試及評估網頁效能,確保這些取捨值得付出努力。

CSS 示範

JavaScript

JavaScript 可在網路上實現大部分的互動功能,但會產生費用。 如果傳送過多 JavaScript,網頁在載入期間的回應速度就會變慢,甚至可能導致回應問題,進而減緩互動速度,這兩者都會讓使用者感到不滿。

會阻礙算繪的 JavaScript

載入沒有 deferasync 屬性的 <script> 元素時,瀏覽器會封鎖剖析和算繪作業,直到指令碼下載、剖析及執行完成為止。同樣地,內嵌指令碼會封鎖剖析器,直到剖析並執行指令碼為止。

asyncdefer

asyncdefer 可讓外部指令碼載入,不會封鎖 HTML 剖析器,而含有 type="module" 的指令碼 (包括內嵌指令碼) 則會自動延遲。不過,asyncdefer 之間有一些重要差異,

這張圖片描繪各種指令碼載入機制,並根據使用的各種屬性 (例如 async、defer、type=&#39;module&#39;,以及這三者的組合),詳細說明剖析器、擷取和執行角色。
資料來源:https://html.spec.whatwg.org/multipage/scripting.html

使用 async 載入的指令碼會在下載後立即剖析及執行,而使用 defer 載入的指令碼則會在 HTML 文件剖析完成時執行,這與瀏覽器的 DOMContentLoaded 事件發生時間相同。此外,async 指令碼可能會依序執行,而 defer 指令碼則會按照在標記中出現的順序執行。

用戶端算繪

一般來說,請避免使用 JavaScript 算繪任何重要內容或網頁的 LCP 元素。這就是所謂的用戶端轉譯,是單頁應用程式 (SPA) 廣泛使用的技術。

JavaScript 轉譯的標記會略過預先載入掃描器,因為掃描器無法探索用戶端轉譯標記中包含的資源。這可能會延遲下載重要資源,例如 LCP 圖片。瀏覽器只會在指令碼執行完畢,並將元素新增至 DOM 後,才開始下載 LCP 圖片。因此,指令碼必須經過探索、下載及剖析,才能執行。這就是所謂的「關鍵要求鏈」,應盡量避免。

此外,使用 JavaScript 算繪標記時,相較於伺服器在回應導覽要求時下載的標記,更有可能產生長時間工作。大量使用 HTML 的用戶端算繪功能可能會對互動延遲時間造成負面影響。如果網頁的 DOM 很大,JavaScript 修改 DOM 時就會觸發大量算繪作業,這時就特別容易發生這種情況。

壓縮

與 CSS 類似,壓縮 JavaScript 可縮減指令碼資源的檔案大小。 這樣可加快下載速度,讓瀏覽器更快進入剖析和編譯 JavaScript 的程序。

此外,JavaScript 的縮小作業比其他素材資源 (例如 CSS) 的縮小作業更進一步。縮減 JavaScript 時,系統不僅會移除空格、定位點和註解等項目,還會縮短來源 JavaScript 中的符號。這項程序有時也稱為「醜化」uglification。如要查看差異,請使用下列 JavaScript 原始碼:

// Unuglified JavaScript source code:
export function injectScript () {
  const scriptElement = document.createElement('script');
  scriptElement.src = '/js/scripts.js';
  scriptElement.type = 'module';

  document.body.appendChild(scriptElement);
}

將上述 JavaScript 原始碼醜化後,結果可能類似下列程式碼片段:

// Uglified JavaScript production code:
export function injectScript(){const t=document.createElement("script");t.src="/js/scripts.js",t.type="module",document.body.appendChild(t)}

在上一個程式碼片段中,您可以看到來源中可供人閱讀的變數 scriptElement 縮短為 t。如果對大量指令碼套用這項功能,節省的費用相當可觀,而且不會影響網站 JavaScript 提供的功能。

如果您使用打包工具處理網站的原始碼,通常會自動對正式版建構作業進行醜化。此外,您也可以高度設定醜化工具 (例如 Terser),調整醜化演算法的積極程度,盡可能節省費用。不過,任何醜化工具的預設值通常都足以在輸出大小和功能保留之間取得適當平衡。

JavaScript 示範

學以致用

在瀏覽器中載入多個 CSS 檔案的最佳方式為何?

多個 <link> 元素。
CSS @import 宣告。

瀏覽器預載掃描器有什麼功用?

偵測 HTML 資源中的 <link rel="preload"> 元素。
這是次要的 HTML 剖析器,會檢查原始標記,以便在 DOM 剖析器之前探索資源,盡早發現資源。

為什麼瀏覽器在下載 JavaScript 資源時,預設會暫時封鎖剖析 HTML?

因為評估 JavaScript 非常耗用 CPU 資源,暫停剖析 HTML 可讓 CPU 有更多頻寬完成指令碼載入作業。
因為指令碼可以修改或存取 DOM。
避免發生未套用樣式的內容閃爍 (FOUC) 問題。

下回預告:使用資源提示輔助瀏覽器

您現在已瞭解在 <head> 元素中載入的資源如何影響初始網頁載入和各種指標,接下來請繼續瞭解其他內容。在下一個單元中,我們將探討資源提示,以及這些提示如何為瀏覽器提供有價值的資訊,讓瀏覽器開始載入資源,並比沒有這些提示時更早開啟與跨來源伺服器的連線。