不要對瀏覽器預先載入掃描器(')

瞭解什麼是瀏覽器預先載入掃描器、如何提升效能,以及如何避免干擾。

最佳化網頁速度時,有一點常被忽略,那就是對瀏覽器內部運作方式的瞭解。瀏覽器會進行特定最佳化,以開發人員無法做到的方式提升效能,但前提是這些最佳化不會意外遭到阻礙。

瀏覽器預先載入掃描器是其中一項內部瀏覽器最佳化功能。本文將說明預先載入掃描器的運作方式,更重要的是,如何避免干擾掃描器。

什麼是預先載入掃描器?

每個瀏覽器都有一個主要的 HTML 剖析器,可將原始標記權杖化,並處理成物件模型。這一切都會順利進行,直到剖析器找到「阻斷資源」時暫停,例如使用 <link> 元素載入的樣式表,或使用 <script> 元素載入的指令碼,但沒有 async 或 defer 屬性。

HTML 剖析器圖表。
圖 1:瀏覽器的主要 HTML 剖析器如何遭到封鎖的圖表。在這種情況下,剖析器會遇到外部 CSS 檔案的 <link> 元素,這會阻止瀏覽器剖析文件的其餘部分,甚至轉譯任何部分,直到 CSS 下載並剖析為止。

如果是 CSS 檔案,系統會封鎖算繪,避免未套用樣式的內容閃爍 (FOUC),也就是在套用樣式前,短暫顯示未套用樣式的網頁版本。

web.dev 首頁處於未設定樣式狀態 (左) 和已設定樣式狀態 (右)。
圖 2:FOUC 模擬範例。左側是 web.dev 的首頁,不含樣式。右側是套用樣式後的同一個網頁。如果瀏覽器在下載及處理樣式表時未封鎖算繪,則可能會在瞬間發生未套用樣式的狀態。

如果瀏覽器遇到不含 defer 或 async 屬性的 <script> 元素,也會停止剖析及轉譯網頁。

這是因為瀏覽器無法確定任何特定指令碼是否會在主要 HTML 剖析器仍在執行時修改 DOM。因此,常見的做法是在文件結尾載入 JavaScript,盡可能降低剖析和算繪作業受阻的影響。

這些都是瀏覽器應該同時封鎖剖析和算繪的充分理由。不過,我們不建議封鎖這兩個重要步驟,因為這會延遲發現其他重要資源,進而拖慢網頁載入速度。幸好,瀏覽器會盡可能透過名為「預先載入掃描器」的次要 HTML 剖析器,減輕這些問題。

主要 HTML 剖析器 (左) 和預先載入掃描器 (右) 的圖表,後者是次要 HTML 剖析器。
圖 3:這張圖表說明預先載入掃描器如何與主要 HTML 剖析器平行運作,推測載入素材資源。在這裡,主要 HTML 剖析器會遭到封鎖,因為它會先載入及處理 CSS,才能開始處理 <body> 元素中的圖片標記,但預先載入掃描器可以預先查看原始標記,找出該圖片資源,並在主要 HTML 剖析器解除封鎖前開始載入。

預先載入掃描器的角色是推測性,也就是說,它會檢查原始標記,以便在主要 HTML 剖析器發現資源之前,找出要擷取的資源。

如何判斷預先載入掃描器是否正在運作

預先載入掃描器之所以存在,是因為轉譯和剖析作業遭到封鎖。如果這兩個效能問題從未存在,預先載入掃描器就不會非常實用。要判斷網頁是否能從預先載入掃描器中受益,關鍵在於這些阻斷現象。為此,您可以對要求引入人工延遲,找出預先載入掃描器的運作位置。

以這個網頁為例,其中包含基本文字和圖片,並使用樣式表。由於 CSS 檔案會阻擋算繪和剖析,因此您透過 Proxy 服務,為樣式表設定兩秒的人為延遲。這項延遲可讓您在網路瀑布圖中,更輕鬆地查看預先載入掃描器的運作位置。

WebPageTest 網路瀑布圖顯示,樣式表有人為延遲 2 秒。
圖 4:網頁的 WebPageTest 網路瀑布圖,在行動裝置上透過模擬 3G 連線在 Chrome 上執行。雖然樣式表透過 Proxy 人為延遲兩秒才開始載入,但預先載入掃描器仍會發現標記酬載中較後面的圖片。

如瀑布圖所示,即使算繪和文件剖析作業遭到封鎖,預先載入掃描器仍會發現 <img> 元素。如果沒有這項最佳化,瀏覽器就無法在封鎖期間伺機擷取內容,而且更多資源要求會連續而非同時發生。

在排除這個玩具範例後,我們來看看一些可能導致預先載入掃描器失效的實際模式,以及如何修正這些模式。

插入了 async 個指令碼

假設 <head> 中有 HTML,其中包含一些內嵌 JavaScript,如下所示:

<script>
  const scriptEl = document.createElement('script');
  scriptEl.src = '/yall.min.js';

  document.head.appendChild(scriptEl);
</script>

插入的指令碼預設為 async,因此插入這個指令碼時,其行為會與套用 async 屬性時相同。這表示指令碼會盡快執行,不會阻擋轉譯。聽起來很棒,對吧?不過,如果假設這個內嵌 <script> 位於載入外部 CSS 檔案的 <link> 元素之後,結果會不盡理想:

這張 WebPageTest 圖表顯示,插入指令碼時,預先載入掃描會失敗。
圖 5:WebPageTest 網路瀑布圖,顯示在行動裝置上透過模擬 3G 連線,使用 Chrome 執行網頁的情形。這個網頁包含單一樣式表和插入的 async 指令碼。預先載入掃描器無法在轉譯阻斷階段發現指令碼,因為指令碼是在用戶端插入。

以下詳細說明:

  1. 在 0 秒時,系統會要求主要文件。
  2. 在 1.4 秒時,導覽要求的第一個位元組抵達。
  3. 在 2.0 秒時,系統會要求 CSS 和圖片。
  4. 由於剖析器在載入樣式表時遭到封鎖,且插入 async 指令碼的內嵌 JavaScript 在 2.6 秒時出現在該樣式表「之後」,因此該指令碼提供的功能無法盡快提供。

這並非最佳做法,因為系統會在樣式表下載完成後,才要求指令碼。這會延遲指令碼的執行時間。相較之下,由於 <img> 元素可在伺服器提供的標記中找到,因此預先載入掃描器會發現該元素。

那麼,如果使用含有 async 屬性的一般 <script> 標記,而不是將指令碼插入 DOM,會發生什麼情況?

<script src="/yall.min.js" async></script>

結果如下:

WebPageTest 網路瀑布圖,說明使用 HTML 指令碼元素載入的非同步指令碼,如何仍可由瀏覽器預先載入掃描器偵測,即使瀏覽器的主要 HTML 剖析器在下載及處理樣式表時遭到封鎖。
圖 6:在行動裝置上透過模擬 3G 連線,在 Chrome 上執行的網頁 WebPageTest 網路瀑布圖。這個網頁包含單一樣式表和單一 async <script> 元素。預先載入掃描器會在轉譯阻斷階段發現指令碼,並與 CSS 同時載入。

您可能會想建議使用 rel=preload 解決這些問題。這個方法確實可行,但可能會產生一些副作用。畢竟,如果可以「不」將 <script> 元素插入 DOM 來避免問題,何必使用 rel=preload 修正問題呢?

WebPageTest 瀑布圖顯示如何使用 rel=preload 資源提示,提升非同步插入指令碼的探索速度,但這種做法可能會產生非預期的副作用。
圖 7:網頁的 WebPageTest 網路瀑布圖,在行動裝置上透過模擬 3G 連線在 Chrome 上執行。這個網頁包含單一樣式表和插入的 async 指令碼,但 async 指令碼會預先載入,確保能更快找到。

預先載入可「修正」這個問題,但會產生新問題:前兩個示範中的 async 指令碼 (雖然在 <head> 中載入) 會以「低」優先順序載入,而樣式表則會以「最高」優先順序載入。在最後一個預先載入 async 指令碼的示範中,樣式表仍以「最高」優先順序載入,但指令碼的優先順序已提升為「高」。

當資源的優先順序提高時,瀏覽器會為其分配更多頻寬。這表示即使樣式表的優先順序最高,指令碼的優先順序提高仍可能導致頻寬競爭。如果連線速度緩慢,或資源相當龐大,就可能發生這種情況。

這個問題的答案很簡單:如果啟動期間需要指令碼,請勿將指令碼插入 DOM,以免預先載入掃描器失效。視需要嘗試調整 <script> 元素位置,以及 defer 和 async 等屬性。

使用 JavaScript 延遲載入

延遲載入是節省數據用量的好方法,通常用於圖片。不過,有時延遲載入會錯誤套用至「不需捲動位置」的圖片。

這會導致預先載入掃描器在資源曝光度方面發生潛在問題,並可能不必要地延遲發現圖片參照、下載、解碼及呈現圖片所需的時間。以下列圖片標記為例:

<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">

使用 data- 前置字元是 JavaScript 延遲載入器的常見模式。當圖片捲動至可視區域時,延遲載入器會移除 data- 前置字元,也就是說,在上述範例中,data-src 會變成 src。這項更新會提示瀏覽器擷取資源。

如果將這個模式套用至啟動期間檢視區塊中的圖片,就會發生問題。由於預先載入掃描器讀取 data-src 屬性的方式與 src (或 srcset) 屬性不同,因此無法提早發現圖片參照。更糟的是,圖片會延遲載入,直到延遲載入器 JavaScript 下載、編譯和執行之後才會載入。

WebPageTest 網路刊登序列圖表,顯示在啟動期間,可視區域中的延遲載入圖片如何延遲,因為瀏覽器預先載入掃描器找不到圖片資源,且只有在延遲載入所需的 JavaScript 載入時才會載入。圖片發現時間過晚。
圖 8:在行動裝置上透過模擬 3G 連線,使用 Chrome 執行網頁的 WebPageTest 網路瀑布圖。圖片資源在啟動期間會顯示在可視區域中,但仍會延遲載入。這會導致預先載入掃描器失效,並造成不必要的延遲。

視圖片大小而定 (可能取決於可視區域大小),圖片可能是最大內容繪製 (LCP) 的候選元素。如果預先載入掃描器無法預先擷取圖片資源(可能是在網頁樣式表封鎖算繪時),LCP 就會受到影響。

解決方法是變更圖片標記:

<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">

如果圖片在啟動期間位於可視區域內,這是最佳模式,因為預先載入掃描器會更快發現並擷取圖片資源。

WebPageTest 網路刊登序列圖,描繪啟動期間可視區域中圖片的載入情境。圖片不會延遲載入,也就是說,圖片不需依附指令碼才能載入,因此預先載入掃描器可以更快找到圖片。
圖 9:在行動裝置上透過模擬 3G 連線,在 Chrome 上執行網頁的 WebPageTest 網路瀑布圖。預先載入掃描器會在 CSS 和 JavaScript 開始載入前,先找出圖片資源,讓瀏覽器能提早載入。

在這個簡化範例中,結果是在連線速度緩慢時,LCP 縮短了 100 毫秒。這看起來可能不是很大的改善,但考量到解決方案只是快速修正標記,且大多數網頁都比這組範例更複雜,這項改善其實相當顯著。這表示 LCP 候選項目可能必須與許多其他資源爭奪頻寬,因此這類最佳化作業越來越重要。

CSS 背景圖片

請注意,瀏覽器預先載入掃描器會掃描標記。不會掃描其他資源類型,例如 CSS,這類資源可能會擷取 background-image 屬性參照的圖片。

與 HTML 類似,瀏覽器會將 CSS 處理成自己的物件模型,稱為 CSSOM。如果在建構 CSSOM 時發現外部資源,系統會在發現時要求這些資源,而不是由預先載入掃描器要求。

假設網頁的 LCP 候選元素是具有 CSS background-image 屬性的元素。載入資源時會發生以下情況:

WebPageTest 網路瀑布圖,顯示使用 background-image 屬性從 CSS 載入 LCP 候選元素的網頁。由於 LCP 候選圖片的資源類型無法由瀏覽器預先載入掃描器檢查,因此資源會延遲載入,直到 CSS 下載並處理完畢,導致 LCP 候選圖片的繪製時間延遲。
圖 10:WebPageTest 網路瀑布圖,顯示網頁在行動裝置上透過模擬 3G 連線在 Chrome 上執行的情況。網頁的 LCP 候選元素是具有 CSS background-image 屬性的元素 (第 3 列)。CSS 剖析器找到圖片後,才會開始擷取要求的圖片。

在這種情況下,預先載入掃描器並非無法運作,而是未參與。即便如此,如果網頁上的 LCP 候選元素來自 background-image CSS 屬性,建議您預先載入該圖片:

<!-- Make sure this is in the <head> below any
     stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">

rel=preload 提示雖然很小,但能協助瀏覽器更快找到圖片:

WebPageTest 網路刊登序列圖表,顯示 CSS 背景圖片 (LCP 候選圖片) 載入速度較快,因為使用了 rel=preload 提示。LCP 時間縮短約 250 毫秒。
圖 11:WebPageTest 網路瀑布圖,顯示在行動裝置上透過模擬 3G 連線,使用 Chrome 執行網頁。網頁的 LCP 候選元素是具有 CSS background-image 屬性的元素 (第 3 列)。rel=preload 提示可協助瀏覽器比沒有提示時提早約 250 毫秒發現圖片。

有了 rel=preload 提示,系統就能更快找到 LCP 候選元素,縮短 LCP 時間。雖然這個提示有助於修正問題,但更好的做法可能是評估是否必須從 CSS 載入圖片 LCP 候選項目。使用 <img> 標記,您就能進一步控管如何載入適合可視區域的圖片,同時允許預先載入掃描器探索圖片。

內嵌過多資源

內嵌是指將資源置於 HTML 內。您可以在 <style> 元素中內嵌樣式表、在 <script> 元素中內嵌指令碼,以及使用 base64 編碼內嵌幾乎任何其他資源。

內嵌資源的速度可能比下載資源更快,因為系統不會針對資源發出個別要求。這項功能就在文件中,而且載入速度很快。不過,這種做法有重大缺點:

  • 如果未快取 HTML (如果 HTML 回應是動態的,就無法快取),內嵌資源就永遠不會快取。由於內嵌資源無法重複使用,因此會影響成效。
  • 即使您可以快取 HTML,內嵌資源也不會在文件之間共用。相較於可在整個來源快取及重複使用的外部檔案,這種做法會降低快取效率。
  • 如果內嵌過多內容,預先載入掃描器會延遲探索文件後續的資源,因為下載額外的內嵌內容需要較長時間。

以這個網頁為例,在某些情況下,LCP 候選元素是頁面頂端的圖片,而 CSS 則位於由 <link> 元素載入的獨立檔案中。這個網頁也使用四種網路字型,這些字型是從 CSS 資源中以個別檔案的形式要求。

網頁的 WebPageTest 網路瀑布圖,其中包含外部 CSS 檔案,內含四個字型。預先載入掃描器會在適當時間偵測到 LCP 候選圖片。
圖 12:WebPageTest 網路瀑布圖,顯示在行動裝置上透過模擬 3G 連線,使用 Chrome 執行網頁的情形。網頁的 LCP 候選項目是從 <img> 元素載入的圖片,但預先載入掃描器會發現該圖片,因為網頁所需的 CSS 和字型會載入不同的資源,不會延遲預先載入掃描器的工作。

如果 CSS 和所有字型都內嵌為 Base64 資源,會發生什麼情況?

WebPageTest 網路瀑布圖,顯示含有外部 CSS 檔案的網頁,其中參照了四種字型。預先載入掃描器偵測到 LCP 圖片的時間大幅延遲。
圖 13:在行動裝置上透過模擬 3G 連線,在 Chrome 上執行的網頁 WebPageTest 網路瀑布圖。網頁的 LCP 候選項目是從 <img> 元素載入的圖片,但 CSS 及其四個字型資源在 `` 中的內嵌會延遲預先載入掃描器發現圖片,直到這些資源完全下載為止。

在這個範例中,內嵌會對 LCP 造成負面影響,對整體效能也是如此。未內嵌任何內容的網頁版本,會在約 3.5 秒內繪製 LCP 圖片。內嵌所有內容的網頁,則要到 7 秒後才會繪製 LCP 圖片。

這裡不只有預先載入掃描器。內嵌字型並非理想策略,因為 base64 是二進位資源的低效率格式。另一個因素是,除非 CSSOM 判定有必要,否則不會下載外部字型資源。如果這些字型以 base64 內嵌,無論目前網頁是否需要,都會下載。

預先載入是否能改善這裡的情況?沒問題,您可以預先載入 LCP 圖片並縮短 LCP 時間,但如果使用內嵌資源,導致可能無法快取的 HTML 膨脹,會對效能造成其他負面影響。首次顯示內容所需時間 (FCP) 也會受到這種模式影響。在未內嵌任何內容的網頁版本中,FCP 約為 2.7 秒。在所有內容都內嵌的版本中,FCP 大約是 5.8 秒。

將內容內嵌至 HTML 時,請務必小心,尤其是 base64 編碼資源。一般來說,除非資源非常小,否則不建議這麼做。盡可能減少內嵌,因為內嵌過多會造成問題。

使用用戶端 JavaScript 轉譯標記

毫無疑問,JavaScript 絕對會影響網頁速度。開發人員不僅仰賴 JavaScript 提供互動功能,也傾向使用 JavaScript 傳遞內容。這在某些方面可提升開發人員體驗,但對開發人員有利不一定對使用者有利。

使用用戶端 JavaScript 轉譯標記,是可能導致預先載入掃描器失效的模式之一:

WebPageTest 網路瀑布圖,顯示基本網頁,其中圖片和文字完全在用戶端以 JavaScript 轉譯。由於標記包含在 JavaScript 中,預先載入掃描器無法偵測到任何資源。由於 JavaScript 架構需要額外的網路和處理時間,所有資源都會額外延遲。
圖 14:WebPageTest 網路瀑布圖,顯示在行動裝置上透過模擬 3G 連線,使用 Chrome 執行用戶端轉譯網頁的情形。由於內容包含在 JavaScript 中,且依賴架構算繪,因此用戶端算繪標記中的圖片資源會對預先載入掃描器隱藏。圖 9 則顯示伺服器轉譯的對等體驗。

如果標記酬載包含在瀏覽器中的 JavaScript 中,且完全由 JavaScript 轉譯,則預先載入掃描器實際上無法看到該標記中的任何資源。這會延遲重要資源的探索,進而影響 LCP。在這些範例中,與不需要 JavaScript 即可顯示的對等伺服器算繪體驗相比,LCP 圖片的要求會大幅延遲。

這與本文重點稍有不同,但在用戶端轉譯標記的效果,不只是擊敗預先載入掃描器。首先,如果導入 JavaScript 來支援不需要 JavaScript 的體驗,會增加不必要的處理時間,進而影響 Interaction to Next Paint (INP)。相較於伺服器傳送的相同標記量,在用戶端轉譯大量標記,更有可能產生長時間任務。除了 JavaScript 涉及額外處理作業外,另一個原因是瀏覽器會從伺服器串流標記,並以傾向限制長時間任務的方式區塊轉譯。另一方面,用戶端轉譯的標記會以單一、龐大的工作處理,可能會影響網頁的 INP。

這種情況的解決方法取決於以下問題的答案:是否有任何原因導致網頁標記無法由伺服器提供,而必須在用戶端轉譯?如果答案是「否」,請盡可能考慮使用伺服器端轉譯 (SSR) 或靜態產生的標記,因為這有助於預先載入掃描器提前發現並伺機擷取重要資源。

如果網頁確實需要 JavaScript 才能將功能附加至網頁標記的某些部分,您仍可透過 SSR 執行這項操作,方法是使用 Vanilla JavaScript 或凍結,兩全其美。

協助預先載入掃描器

預先載入掃描器是高效的瀏覽器最佳化工具,可加快網頁啟動時的載入速度。避免使用會阻礙預先載入掃描器探索重要資源的模式,不僅能簡化開發作業,還能打造更優質的使用者體驗,進而提升多項指標的成效,包括部分網站體驗指標。

最後,我們來複習一下本指南的重點:

  • 瀏覽器預先載入掃描器是次要 HTML 剖析器,會在主要剖析器遭到封鎖時,提前掃描主要剖析器,以便盡早找出可擷取的資源。
  • 如果資源未出現在伺服器在初始導覽要求中提供的標記中,預先載入掃描器就無法偵測到。預先載入掃描器可能失效的情況包括 (但不限於):
    • 使用 JavaScript 將資源插入 DOM,無論是指令碼、圖片、樣式表,還是任何其他資源,最好都放在伺服器的初始標記酬載中。
    • 使用 JavaScript 解決方案延遲載入不需捲動位置的圖片或 iframe。
    • 在用戶端上轉譯標記,其中可能包含使用 JavaScript 參照文件子資源。
  • 預先載入掃描器只會掃描 HTML。不會檢查其他資源 (尤其是 CSS) 的內容,這些資源可能包含重要素材資源 (包括 LCP 候選項目) 的參照。

如果基於任何原因,您無法避免會對預先載入掃描器加快載入效能的能力造成負面影響的模式,請考慮使用 rel=preload 資源提示。如果您確實使用 rel=preload,請在實驗室工具中測試,確保能達到預期效果。最後,請勿預先載入過多資源,因為如果所有資源都優先載入,就等於沒有優先順序。

資源