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

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

在進行網頁速度最佳化時,有一個容易被忽略的部分,就是需要瞭解一些瀏覽器內部運作方式。瀏覽器會進行特定最佳化作業,以便提升效能,這也是開發人員無法做到的,但前提是這些最佳化作業不會遭到無意阻撓。

其中一個內部瀏覽器最佳化項目是瀏覽器預先載入掃描器。本文將說明預先載入掃描器的運作方式,以及更重要的是如何避免妨礙其運作。

什麼是預載掃描器?

每個瀏覽器都有一個主要的 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 來解決這些問題。這麼做肯定可行,但可能會帶來一些副作用。畢竟,如果可以<script> 元素插入 DOM 來避免問題,為何要使用 rel=preload 來修正問題?

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: 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 變得臃腫,進而導致其他負面效應。首次顯示內容所需時間 (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 圖片的要求延遲的程度「明顯」(significantly)。

這與本文重點稍有出入,但在用戶端上顯示標記的效果遠遠超出避開預先載入掃描器的範圍。首先,如果為不需要 JavaScript 的體驗導入 JavaScript,就會造成不必要的處理時間,進而影響 Interaction to Next Paint (INP)。與伺服器傳送的相同標記量相比,在用戶端算繪大量標記時,更有可能產生長時間工作。除了 JavaScript 涉及的額外處理作業外,瀏覽器會從伺服器串流標記,並以傾向限制長時間作業的方式分割轉譯作業。另一方面,由用戶端轉譯的標記會以單一整體工作處理,這可能會影響網頁的 INP。

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

如果網頁確實需要使用 JavaScript 將功能附加至網頁標記的部分,您仍可透過 SSR 執行此操作,無論是使用一般 JavaScript 或重新整理,都能發揮兩者的優點。

讓預先載入掃描工具協助您

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

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

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

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

資源

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