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

瞭解瀏覽器預先載入掃描器的功能、對效能帶來的助益,以及如何避免受到影響。

在進行網頁速度最佳化時,有一個容易被忽略的部分,就是需要瞭解一些瀏覽器內部運作方式。瀏覽器會執行某些最佳化作業,以達到開發人員無法預期的方式提升效能,前提是瀏覽器必須避免無意間受到最佳化處理。

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

什麼是預載掃描器?

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

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

如果是 CSS 檔案,系統會禁止轉譯,以免閃爍內容 (FOUC) 內容,也就是在套用樣式前,短暫顯示未設定樣式的網頁版本。

web.dev 首頁在未設定樣式 (左圖) 和設定樣式 (右圖) 的狀態。
圖 2:模擬的 FOUC 範例。左側是未套用樣式的 web.dev 首頁。右側是套用樣式的相同網頁。如果瀏覽器在下載及處理樣式表時未阻擋算繪作業,就可能會在瞬間出現未設定樣式的狀態。

瀏覽器也會在遇到沒有 deferasync 屬性的 <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. 由於剖析器會阻擋載入樣式表單,而會在 2.6 秒後在樣式表單後插入 async 指令碼的內嵌式 JavaScript,因此指令碼提供的功能無法盡快提供。

這不是最佳做法,因為只有在樣式表下載完成後,系統才會要求指令碼。這會延遲指令碼盡快執行的時間。相反地,由於伺服器提供的標記容易找到 <img> 元素,因此預先載入掃描器會找到這個元素。

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

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

結果如下:

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

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

WebPageTest 階層圖表,顯示如何使用 rel=preload 資源提示,促進非同步插入指令碼的偵測作業,但可能會產生非預期的副作用。
圖 7:WebPageTest 網路瀑布圖表,顯示在模擬 3G 連線的行動裝置上,透過 Chrome 執行網頁的結果。頁面包含單一樣式表和插入的 async 指令碼,但 async 指令碼會預先載入,以便盡快發現。

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

資源的優先順序提高後,瀏覽器會為該資源分配更多頻寬。這表示,即使樣式表的優先順序最高,但指令碼的提高優先順序可能會導致頻寬爭用。這可能是連線速度緩慢或資源過大的因素。

答案很簡單:如果啟動期間需要指令碼,請勿將指令碼插入 DOM 以破壞預先載入掃描器。視需要嘗試使用 <script> 元素放置方式,以及 deferasync 等屬性。

使用 JavaScript 延遲載入

延遲載入是節省資料的好方法,通常會套用在圖片上。不過,有時延遲載入功能會錯誤套用至「上方」圖片。

這導致預先載入掃描器會遇到資源探索的問題,也會無必要延遲探索圖片參照、下載、解碼及呈現資源所需的時間。以這個圖片標記為例:

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

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

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

與 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 時間。雖然這項提示有助於解決這個問題,但更好的做法或許是評估圖片 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 格式內嵌時,無論目前網頁是否需要這些字型,系統都會下載這些字型。

預先載入功能是否有助於改善這個問題?沒問題。您可以預先載入 LCP 圖片,並縮短 LCP 時間,但如果使用內嵌資源來膨脹可能無法快取的 HTML,就會導致其他負面效能後果。首次顯示內容所需時間 (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 的體驗導入 JavaScript,就會造成不必要的處理時間,進而影響 Interaction to Next Paint (INP)。與伺服器傳送的相同標記量相比,在用戶端算繪大量標記時,更有可能產生長時間工作。除了 JavaScript 涉及的額外處理作業外,瀏覽器會從伺服器串流標記,並以傾向限制長時間作業的方式分割轉譯作業。另一方面,由用戶端轉譯的標記會以單一整體工作處理,這可能會影響網頁的 INP。

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

如果網頁確實需要使用 JavaScript 將功能附加至網頁標記的部分,您仍可透過 SSR 執行此操作,無論是使用一般 JavaScript 或補充,都能同時享有兩者的優點。

協助預先載入掃描器

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

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

  • 瀏覽器預先載入掃描器是次要 HTML 剖析器,會在主要剖析器遭到封鎖時先行掃描,以便機緣巧遇地發現可提早擷取的資源。
  • 在初始導覽要求中,如果伺服器提供的標記中沒有資源,預先載入掃描器就無法偵測到這些資源。預先載入掃描器可能會遭到破解的方式包括 (但不限於) 以下:
    • 使用 JavaScript 在 DOM 插入資源,可以是指令碼、圖片、樣式表,或者是其他適合來自伺服器的初始標記酬載。
    • 使用 JavaScript 解決方案延遲載入不需捲動位置的圖片或 iframe。
    • 在用戶端上算繪標記,可能包含使用 JavaScript 的文件子資源參照。
  • 預載掃描器只會掃描 HTML。但不會檢查其他資源 (尤其是 CSS) 的內容,因為這些資源可能會參照重要資產,包括 LCP 候選項目。

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

資源

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