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

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

Page Speed 最佳化有一項容易忽略的環節,就是稍微瞭解瀏覽器內部運作方式。瀏覽器會進行某些最佳化作業,以提升效能,這是開發人員無法做到的,但前提是這些最佳化作業不會遭到意外阻撓。

其中一項值得瞭解的內部瀏覽器最佳化功能是瀏覽器預先載入掃描器。本文將說明預先載入掃描器的運作方式,以及如何避免妨礙掃描器運作。

什麼是預先載入掃描器?

每個瀏覽器都有主要 HTML 剖析器,可將原始標記權杖化,並處理成物件模型。這個過程會持續進行,直到剖析器發現會造成阻斷的資源為止,例如以 <link> 元素載入的樣式表,或是以 <script> 元素載入的指令碼 (沒有 asyncdefer 屬性)。

HTML 剖析器圖表。
圖 1:瀏覽器主要 HTML 剖析器遭到封鎖的示意圖。在本例中,剖析器會遇到外部 CSS 檔案的 <link> 元素,導致瀏覽器無法剖析其餘文件,甚至無法算繪任何內容,直到 CSS 下載並剖析完畢為止。

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

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

如果瀏覽器遇到沒有 deferasync 屬性的 <script> 元素,也會封鎖網頁的剖析和轉譯作業。

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

因此瀏覽器應該封鎖剖析和算繪作業。然而,封鎖這兩個重要步驟都不理想,因為這會延遲探索其他重要資源,導致節目延播。幸好,瀏覽器會盡力透過稱為「預先載入掃描器」的次要 HTML 剖析器,減輕這些問題。

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

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

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

由於遭到封鎖而無法算繪和剖析,因此存在預先載入掃描器。如果這兩個效能問題從未存在,預先載入掃描器就沒有太大用處。如要判斷網頁是否能從預載掃描器獲益,關鍵在於這些阻斷現象。為此,您可以為要求導入人為延遲,找出預先載入掃描器運作的位置。

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

WebPageTest 網路瀑布圖顯示樣式表強制延遲 2 秒。
圖 4:在模擬 3G 連線的行動裝置上,透過 Chrome 執行的網頁 WebPageTest 網路瀑布圖。即使樣式表透過 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:在行動裝置上透過模擬的 3G 連線,在 Chrome 中執行的網頁 WebPageTest 網路瀑布圖。這個頁面包含單一樣式表和插入的 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:在行動裝置上透過模擬 3G 連線,在 Chrome 中執行的網頁 WebPageTest 網路瀑布圖。這個網頁包含單一樣式表和插入的 async 指令碼,但 async 指令碼已預先載入,確保系統能更快發現。

預先載入「修正」了這裡的問題,但會產生新問題:前兩個範例中的 async 指令碼 (即使載入於 <head> 中) 是以「低」優先順序載入,而樣式表是以「最高」優先順序載入。在最後一個預先載入  指令碼的範例中,樣式表仍以「最高」優先順序載入,但指令碼的優先順序已提升至「高」。async

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

答案很簡單:如果啟動期間需要指令碼,請勿將指令碼插入 DOM,以免預先載入掃描器失效。視需要實驗 <script> 元素的放置位置,以及 deferasync 等屬性。

使用 JavaScript 延遲載入

延遲載入是節省資料的絕佳方法,通常用於圖片。不過,有時延遲載入會誤用在「不需捲動位置」的圖片上。

這可能會導致資源探索問題,進而影響預先載入掃描器,並不必要地延遲探索圖片參照、下載、解碼及呈現圖片的時間。以這個圖片標記為例:

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

在 JavaScript 驅動的延遲載入器中,使用 data- 前置字串是常見模式。當圖片捲動至可視區域時,延遲載入器會移除 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 背景圖片

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

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

假設網頁的最大內容繪製候選項目是具有 CSS background-image 屬性的元素。資源載入時會發生下列情況:

這張 WebPageTest 網路瀑布圖顯示網頁,其中 LCP 候選項目是使用 background-image 屬性從 CSS 載入。由於 LCP 候選圖片的資源類型無法由瀏覽器預先載入掃描器檢查,因此資源會延遲載入,直到 CSS 下載並處理完畢為止,進而延遲 LCP 候選項目的繪製時間。
圖 10:在行動裝置上透過模擬的 3G 連線,在 Chrome 中執行的網頁 WebPageTest 網路瀑布圖。網頁的 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 網路瀑布圖表顯示,由於使用 rel=preload 提示,CSS 背景圖片 (即 LCP 候選項目) 的載入時間大幅縮短。LCP 時間大約縮短了 250 毫秒。
圖 11:在行動裝置上透過模擬的 3G 連線,在 Chrome 中執行的網頁 WebPageTest 網路瀑布圖。網頁的 LCP 候選元素是具有 CSS background-image 屬性的元素 (第 3 列)。rel=preload 提示可協助瀏覽器更快發現圖片,大約可提早 250 毫秒。

有了 rel=preload 提示,系統就能更快發現 LCP 候選元素,進而縮短 LCP 時間。雖然這項提示有助於修正問題,但更好的做法是評估圖片 LCP 候選項目是否必須從 CSS 載入。使用 <img> 標記,您就能更妥善地控制適合檢視區塊的圖片載入作業,同時允許預先載入掃描器探索圖片。

內嵌過多資源

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

內嵌資源比下載資源更快,因為系統不會為資源發出個別要求。這項資訊會直接顯示在文件中,並立即載入。不過,這項技術有重大缺點:

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

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

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

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

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

在這個範例中,內嵌作業會對 LCP 造成負面影響,一般效能也會受到影響。未內嵌任何內容的網頁版本會在約 3.5 秒內繪製 LCP 圖片。內嵌所有內容的網頁要過 7 秒才會繪製 LCP 圖片。

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

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

請務必謹慎處理 HTML 內嵌內容,尤其是 base64 編碼的資源。一般來說,除非資源非常小,否則不建議這麼做。盡量減少內嵌,因為內嵌過多會造成危險。

使用用戶端 JavaScript 轉譯標記

毫無疑問,JavaScript 一定會影響網頁速度。開發人員不僅依賴這項技術提供互動功能,也傾向於使用這項技術傳送內容。這在某些方面可提升開發人員體驗,但開發人員的優勢不一定能轉化為使用者的優勢。

使用用戶端 JavaScript 算繪標記,可能會導致預先載入掃描器無法運作:

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

如果標記酬載完全由瀏覽器中的 JavaScript 算繪,則該標記中的任何資源對預先載入掃描器而言都有效隱藏。這會延遲重要資源的探索,進而影響 LCP。以這些範例來說,與不需要 JavaScript 顯示的同等伺服器算繪體驗相比,LCP 圖片的要求明顯延遲。

這與本文的重點有些出入,但將標記在用戶端上算繪的效果,遠遠不只於擊敗預先載入掃描器。舉例來說,如果體驗不需要 JavaScript,但您導入了 JavaScript,就會增加不必要的處理時間,進而影響「Interaction to Next Paint」(INP)。與伺服器傳送相同數量的標記相比,在用戶端算繪大量標記時,更有可能產生長時間工作。除了 JavaScript 涉及的額外處理作業外,瀏覽器會從伺服器串流標記,並將轉譯作業分塊,因此往往會限制長時間工作。另一方面,用戶端轉譯的標記會以單一的整體工作處理,這可能會影響網頁的 INP。

這個情況的解決方法取決於這個問題的答案:是否有原因導致伺服器無法提供網頁的標記,而必須在用戶端上算繪?如果答案是「否」,請盡可能考慮使用伺服器端算繪 (SSR) 或靜態產生的標記,因為這有助於預先載入掃描器探索及擷取重要資源。

如果網頁需要 JavaScript 將功能附加至網頁標記的某些部分,您仍可透過 SSR 執行這項操作,方法是使用原生 JavaScript 或Hydration,兼顧兩者優點。

讓預先載入掃描器協助您

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

總結來說,這篇文章的重點如下:

  • 如果主要 HTML 剖析器遭到封鎖,瀏覽器預載掃描器就會在主要剖析器之前掃描,以便盡早發現可擷取的資源。
  • 預先載入掃描器無法探索伺服器在初始導覽要求中提供的標記中不存在的資源。規避預先載入掃描器的方法可能包括 (但不限於):
    • 使用 JavaScript 將資源插入 DOM,無論是指令碼、圖片、樣式表,還是其他最好來自伺服器初始標記酬載的項目。
    • 使用 JavaScript 解決方案,延遲載入首要內容中的圖片或 iframe。
    • 在用戶端上算繪標記,其中可能包含使用 JavaScript 參照文件子資源的內容。
  • 預載掃描器只會掃描 HTML。但不會檢查其他資源的內容 (尤其是 CSS),這些資源可能包含重要資產的參照,包括 LCP 候選項目。

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

資源

主頁橫幅圖片取自 Unsplash,由 Mohammad Rahmani 拍攝。