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

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

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

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

什麼是預載掃描器?

每個瀏覽器都有主要的 HTML 剖析器,可將原始標記代碼化並處理為物件模型。這一切都會順利進行,直到剖析器發現阻斷資源 (例如使用 <link> 元素載入的樣式表,或是使用 <script> 元素載入的劇本,但沒有 asyncdefer 屬性) 時才會暫停。

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 開始載入前,藉由人為延遲的方式經過 2 秒才載入,但預先載入掃描程式可以找到後來標記酬載中的圖片。

如同您在時間軸中看到的,預載掃描器會在顯示和文件剖析遭到阻擋時,發現 <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 修正這些問題,這種做法雖然有效,但可能會有副作用。畢竟,為什麼使用 rel=preload 來修正「不」在 DOM 插入 <script> 元素可避免的問題?

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。這項更新會提示瀏覽器擷取資源。

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

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

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

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

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

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

WebPageTest 網路刊登序列圖表,呈現啟動期間可視區域中圖片的載入情境。圖片不會以延遲載入的方式載入,也就是說,圖片不會依賴指令碼載入,因此預先載入掃描器可以更快地偵測到圖片。
圖 9:WebPageTest 網路階層圖表,顯示在模擬 3G 連線的行動裝置上,透過 Chrome 執行網頁的結果。預先載入掃描器會在 CSS 和 JavaScript 開始載入之前探索圖片資源,讓瀏覽器搶先載入圖片。

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

CSS 背景圖片

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

與 HTML 一樣,瀏覽器會將 CSS 處理至其專屬的物件模型,也就是 CSSOM。如果在建構 CSSOM 時發現外部資源,這些資源是在探索時提出,而非預先載入掃描器。

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

WebPageTest 聯播網刊登序列圖表,呈現使用背景圖片屬性從 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 網路刊登序列圖表,顯示由於使用 rel=preload 提示,CSS 背景圖片 (LCP 候選) 會更快載入。LCP 時間縮短了約 250 毫秒。
圖 11:WebPageTest 網路階層圖表,顯示在模擬 3G 連線的行動裝置上,透過 Chrome 執行網頁的情況。網頁的 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:一份 WebPageTest 網路瀑布圖,顯示在行動裝置上透過模擬 3G 連線在 Chrome 上執行的網頁。網頁的 LCP 候選圖片是從 <img> 元素載入的圖片,但預先載入掃描器會發現這些圖片,因為載入頁面所需的 CSS 和字型是放在個別資源中,不會延遲預先載入掃描器執行工作。

如果 CSS「和」所有字型都內嵌成 base64 資源,會發生什麼事?

WebPageTest 網路刊登序列圖,內含一個外部 CSS 檔案參照的四個字型。發現 LCP 圖片 時,預先載入掃描器已大幅延遲。
圖 13:WebPageTest 網路階層圖表,顯示在模擬 3G 連線的行動裝置上,透過 Chrome 執行網頁的結果。網頁的 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:一份 WebPageTest 網路瀑布圖,顯示使用者在行動裝置上透過模擬 3G 連線在 Chrome 上執行的用戶端轉譯網頁。由於內容包含在 JavaScript 中,並且依賴架構來算繪,因此用戶端算繪標記中的圖片資源會隱藏在預先載入掃描器中。對應的伺服器算繪體驗則列於圖9

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

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

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

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

協助預先載入掃描器

預先載入掃描器是一項高效的瀏覽器最佳化功能,可加快網頁啟動期間的載入速度。避免使用會妨礙系統提前發現重要資源的模式,不僅可簡化開發作業,還能打造更優質的使用者體驗,進而提升許多指標 (包括部分 Web Vitals) 的結果。

總結來說,以下是您從這篇文章中學到的內容:

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

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

資源

主頁橫幅:Unsplash 提供,來源:Mohammad Rahmani