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

瞭解瀏覽器預先載入掃描器是什麼、它如何改善效能,以及如何避免這種掃描器。

對網頁速度最佳化來說,有一個重點不在於對瀏覽器內部的瞭解。瀏覽器會執行某些最佳化作業,以達到開發人員無法預期的方式提升效能,前提是瀏覽器必須避免無意間受到最佳化處理。

要瞭解的內部瀏覽器最佳化功能,就是瀏覽器預先載入掃描器。本文章將說明預先載入掃描器的運作方式,更重要的是,如何避免遭到入侵。

什麼是預先載入掃描器?

每個瀏覽器都有主要的 HTML 剖析器,可將原始標記代碼化並處理為物件模型。在剖析器找到封鎖資源 (例如帶有 <link> 元素載入的樣式表),或是載入不含 asyncdefer 屬性 <script> 元素的指令碼時,系統會停止執行這些操作。

HTML 剖析器圖表。
圖 1:說明如何封鎖瀏覽器主要 HTML 剖析器的圖表。在這種情況下,剖析器會執行外部 CSS 檔案的 <link> 元素,在下載及剖析 CSS 之前,防止瀏覽器剖析文件的其他部分,甚至算繪文件的其他部分。

如果是 CSS 檔案,系統會一併封鎖剖析和轉譯作業,以免出現未樣式的內容閃爍 (FOUC),也就是在套用樣式前,短暫顯示未設定樣式的網頁版本。

web.dev 首頁處於未樣式狀態 (左側) 和樣式設定狀態 (右側)。
圖 2:FOUC 的模擬範例。左側是沒有樣式的 web.dev 首頁。右邊是套用樣式的頁面。如果瀏覽器在下載及處理樣式表時不會阻擋轉譯,可能會在 Flash 中發生未樣式的狀態。

當瀏覽器接觸到沒有 deferasync 屬性的 <script> 元素時,也會禁止剖析及轉譯網頁。

這是因為瀏覽器無法確定是否有任何特定指令碼會在主要 HTML 剖析器還在執行工作時修改 DOM。因此常常在文件結尾載入 JavaScript,讓封鎖剖析和轉譯功能發揮效果。

這些可充分瞭解為何瀏覽器應該封鎖剖析和轉譯。當然,封鎖上述任何一個重要步驟並不恰當,因為這類步驟可能會延遲發掘其他重要資源,才能趕上直播。幸好,瀏覽器會採用名為預先載入掃描器的次要 HTML 剖析器,盡可能緩解這些問題。

主要 HTML 剖析器 (左側) 和預先載入掃描器 (右側) 的圖表,載入器是次要的 HTML 剖析器。
圖 3:顯示預先載入掃描器和主要 HTML 剖析器平行運作以推測載入素材資源的圖表。在這裡,主要 HTML 剖析器在載入並處理 CSS 後,才能開始處理 <body> 元素中的圖片標記,但預先載入掃描器可以在原始標記中尋找該圖片資源,並在解除主要 HTML 剖析器的封鎖前開始載入圖片資源。

預先載入掃描器具有推測性,這表示該標記會檢查原始標記,在主要 HTML 剖析器找到這些資源之前,以機會方式擷取資源。

如何判斷預先載入掃描器的運作時間

預先載入掃描器「因為」進行轉譯和剖析作業遭到封鎖,因此。如果這兩個效能問題從未存在,預先載入掃描器就不會非常有用。要判斷網頁是否可從預先載入掃描程式中獲得好處,關鍵在於這些封鎖現象。為此,您可以導入人工延遲,找出預先載入掃描器的位置。

這個頁面為例,說明基本文字和圖片和樣式表範例。由於 CSS 檔案會同時封鎖轉譯和剖析作業,因此您會透過 Proxy 服務,為樣式表導入人工延遲 2 秒。藉由這段延遲,您可以更輕鬆地在預先載入掃描器執行的網路刊登序列中看到。

WebPageTest 網路刊登序列圖顯示出樣式表上的人工延遲時間為 2 秒。
圖 4:顯示網頁WebPageTest 網路瀑布圖,在行動裝置上透過模擬的 3G 連線執行。雖然樣式表會在開始載入前,藉由人為方式延遲透過 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 連線執行。網頁包含一個樣式表和插入的 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>

結果如下:

即使瀏覽器在下載及處理樣式表時,瀏覽器的主要 HTML 剖析器遭到封鎖,仍可找到透過 HTML 指令碼元素載入的非同步指令碼的 WebPageTest 網路刊登序列。
圖 6:顯示網頁的 WebPageTest 網路瀑布圖,以模擬 3G 連線在行動裝置上執行。頁麵包含一個樣式表和一個 async <script> 元素。預先載入掃描器會在轉譯封鎖階段探索指令碼,並與 CSS 並行載入指令碼。

系統也許會推薦使用 rel=preload 修正這些問題,這種做法雖然有效,但可能會有副作用。畢竟,為什麼使用 rel=preload 來修正「不」在 DOM 插入 <script> 元素可避免的問題?

WebPageTest 刊登序列,顯示如何使用 rel=preload 資源提示提升發現非同步插入指令碼的能力,但這不一定有非預期的副作用。
圖 7:顯示網頁的 WebPageTest 網路瀑布圖,在行動裝置上透過模擬 3G 連線執行。網頁含有單一樣式表和插入的 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。這項更新會提示瀏覽器擷取資源。

在啟動期間,這個模式必須套用至可視區域中的圖片,才會造成問題。由於預先載入掃描器不會以處理 src (或 srcset) 屬性的方式讀取 data-src 屬性,因此先前不會發現圖片參照。更糟的是,在延遲載入器 JavaScript 下載、編譯及執行「之後」,圖片必須等到載入後才能載入。

WebPageTest 網路刊登序列圖表,顯示在啟動期間,可視區域中延遲載入的圖片因瀏覽器預先載入掃描器找不到圖片資源而發生延遲,且只會在延遲載入作業所需的 JavaScript 時載入。發現圖片的時間比實際時間長。
圖 8:顯示網頁的 WebPageTest 網路瀑布圖,以模擬 3G 連線在行動裝置上執行。即使圖片資源會在啟動期間顯示在可視區域內,但系統還是不需要延遲載入圖片資源。這會破壞預先載入掃描器,並造成不必要的延遲。

視圖片大小而定 (可能會視可視區域的大小而定),這個圖片可能是最大內容繪製 (LCP) 的候選元素。如果預先載入掃描器無法事先擷取圖片資源,可能在網頁的樣式表區塊轉譯時,LCP 就會發生問題。

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

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

這是啟動期間可視可視區域中圖片的最佳模式,因為預先載入掃描程式會更快發現並擷取圖片資源。

WebPageTest 網路刊登序列圖表,呈現啟動期間可視區域中圖片的載入情境。圖片未延遲載入,因此無須依賴指令碼即可載入,因此預先載入掃描器可更快發現圖片。
圖 9:顯示網頁的 WebPageTest 網路瀑布圖,以模擬 3G 連線在行動裝置上執行。預先載入掃描器會在 CSS 和 JavaScript 開始載入之前探索圖片資源,讓瀏覽器搶先載入圖片。

這個簡化的範例後,在連線速度緩慢的情況下,LCP 改善了 100 毫秒。這個過程似乎有點進步,不過解決問題的重點是快速修正標記,而且大部分網頁都比這一系列範例來得複雜。這表示 LCP 候選者可能必須承受許多其他資源的頻寬,因此這類最佳化越來越重要。

CSS 背景圖片

請注意,瀏覽器預先載入掃描器會掃描標記。系統不會掃描其他資源 (例如 CSS 供應商可能需擷取 background-image 屬性參照的圖片)。

瀏覽器和 HTML 一樣,都會將 CSS 處理成自己的物件模型,稱為 CSSOM。如果在建構 CSSOM 時發現外部資源,這些資源是在探索時提出,而非預先載入掃描器。

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

WebPageTest 聯播網刊登序列圖表,呈現使用背景圖片屬性從 CSS 載入 LCP 候選項目的網頁。由於 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 中的做法。您可以將樣式表內嵌於 <style> 元素、<script> 元素中的指令碼,以及幾乎任何其他資源 (使用 base64 編碼)。

內嵌資源可能比下載資源更快,因為系統不會對資源發出個別要求。可直接在文件中立即載入。不過,仍有重大缺點:

  • 如果您未快取 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 內嵌,則會針對目前網頁是否需要下載。

預先載入可以改善這裡的運作嗎?沒問題。您可以預先載入 LCP 圖片並縮短 LCP 時間,但使用內嵌資源散佈可能無法快取的 HTML 將會產生其他負面的效能後果。First Contentful Paint (FCP) 也會受到這個模式影響。在沒有內嵌元素的網頁中,FCP 約為 2.7 秒。在所有內嵌項目的版本中,FCP 約為 5.8 秒。

將元素嵌入 HTML 時請小心謹慎,尤其是 Base64 編碼資源。除非資源規模過小,否則一般不建議使用。請盡量避免內嵌,因為內嵌過多與火災。

使用用戶端 JavaScript 轉譯標記

這意味著,JavaScript 一定會影響網頁速度。開發人員不只依賴它來提供互動性,也常會仰賴這項功能來提供內容。在某些方面可以改善開發人員體驗,但對開發人員來說,不一定能提升使用者體驗。

有一種模式可以破壞預先載入掃描器,是使用用戶端 JavaScript 轉譯標記:

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

若標記酬載完全由瀏覽器 JavaScript 包含並轉譯,則預先載入掃描器實際上無法察覺該標記中的所有資源。這會導致系統延遲探索重要資源,甚至會影響 LCP。在這些示例中,相較於不需要 JavaScript 顯示的同等伺服器算繪體驗,LCP 圖片的要求明顯延遲。

雖然這稍微超出了本文的重點,但轉譯標記對用戶端的影響遠遠不只超越了預先載入掃描器。第一,導入 JavaScript 來提供不需要的處理時間,進而增加不必要的處理時間,進而影響與下一個顯示的內容互動 (INP)。比起伺服器傳送的標記量一樣,在用戶端轉譯極大量的標記較有可能產生較長的工作。這個問題的原因是,除了 JavaScript 的額外處理程序之外,瀏覽器從伺服器串流標記,並且以較容易限制長時間工作的方式分割算繪。另一方面,用戶端算繪的標記則會視為單一單體工作來處理,進而影響網頁的 INP。

這個情況的解決方法取決於這個問題的答案:網頁標記無法提供給伺服器 (而非在用戶端顯示) 的原因為何?如果答案為「沒有」,請盡可能考慮伺服器端轉譯 (SSR) 或靜態產生的標記,因為這可協助預先載入掃描器事先發現並隨機擷取重要資源。

如果網頁「確實」需要透過 JavaScript 為網頁標記的某些部分附加功能,你還是可以透過基本 JavaScript 使用 SSR 或混合,以獲得最佳兩種結果。

協助預先載入掃描器

預先載入掃描器是一項高效的瀏覽器最佳化功能,可加快網頁啟動期間的載入速度。只要避免特定模式提前發掘重要資源的模式,不僅能簡化開發作業,使用者也能在眾多指標 (包括一些網站體驗指標) 中創造更好的體驗。

總結來說,以下是這篇文章說明:

  • 瀏覽器預先載入掃描程式是一種次要 HTML 剖析器,如果系統遭到阻斷,以便找出更快擷取的資源,便會在主要 HTML 剖析器之前掃描。
  • 伺服器在初始瀏覽要求中提供的標記中沒有的標記,無法預先載入掃描工具所找到的資源。預先載入掃描器可被破解的方式包括但不限於:
    • 使用 JavaScript 在 DOM 插入資源,可以是指令碼、圖片、樣式表,或者是來自伺服器的初始標記酬載中較佳的任何內容。
    • 使用 JavaScript 解決方案延遲載入不需捲動位置的圖片或 iframe。
    • 在用戶端上算繪標記,而標記可能包含使用 JavaScript 的文件子資源參照。
  • 預先載入掃描器只會掃描 HTML。但不會檢查其他資源 (尤其是 CSS) 的內容。這些資源可能含有重要資產 (包括 LCP 候選) 的參考資料。

如果您基於任何原因,「無法」避免特定模式會對預先載入掃描器加速載入效能的能力造成負面影響,請考慮使用 rel=preload 資源提示。如果您「會」使用 rel=preload,請在研究室工具中進行測試,確認是否符合預期。最後,請勿預先載入過多資源,因為如果您排定所有優先次序,會什麼都無法。

資源

主頁橫幅由 Mohammad Rahmani 提供,由 Unsplash 提供。