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

瞭解瀏覽器預先載入掃描器的功能、此功能如何提升效能,以及您可以如何順暢執行此操作。

想要加快網頁速度,其中一個忽然忽視了瀏覽器內部架構。瀏覽器會進行特定最佳化調整,以期改善效能,這是開發人員無法做到的,但前提是這些最佳化作業不會遭到意外阻擋。

瀏覽器預先載入掃描器是內部瀏覽器最佳化作業之一。這篇文章將說明預先載入掃描器的運作方式,以及更重要的是,如何避免發生問題。

什麼是預先載入掃描器?

每個瀏覽器都有主要 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 服務將樣式表導入人工延遲兩秒。這段延遲能讓您更容易在預先載入掃描器運作的網路瀑布中看到。

WebPageTest 網路刊登序列圖顯示樣式表上發生了 2 秒的人工延遲情形。
圖 4:網頁在行動裝置上的 Chrome 使用模擬 3G 連線執行的 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:在行動裝置上的 Chrome 透過模擬 3G 連線執行的 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:在行動裝置上的 Chrome 透過模擬 3G 連線執行的 WebPageTest 網路瀑布圖。網頁包含單一樣式表和一個 async <script> 元素,預先載入掃描器會在轉譯封鎖階段尋找指令碼,並與 CSS 同時載入。

您可能會想使用 rel=preload 來解決這些問題。這種做法雖然有效,但也可能帶來一些副作用。畢竟,為何使用 rel=preload 來解決「不」插入 <script> 元素可避免的問題?

WebPageTest 刊登序列顯示,我們如何使用 rel=preload 資源提示提高發現非同步插入的指令碼 (不過可能會產生意料之外的副作用)。
圖 7:在行動裝置上的 Chrome 透過模擬 3G 連線執行的 WebPageTest 網路刊登序列圖。網頁包含一個樣式表和一個插入的 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。這項更新會提示瀏覽器擷取資源。

除非此模式會在啟動期間套用至可視區域中的圖片,否則這個模式不會有問題。由於預先載入掃描器讀取 data-src 屬性的方式與讀取 src (或 srcset) 屬性的方式不同,因此先前未找到圖片參照。更糟的是,圖片會在延遲載入器 JavaScript 下載、編譯及執行「之後」才載入。

WebPageTest 網路瀑布圖顯示啟動期間,於可視區域中延遲載入的圖片會因為找不到圖片資源而延遲載入,而且只有在延遲載入才能運作時需要 JavaScript 才能載入。系統預計在不久後找到圖片。
圖 8:在行動裝置上的 Chrome 透過模擬 3G 連線執行的 WebPageTest 網路刊登序列圖。即使在啟動期間可看到圖片資源,圖片資源仍因此不需要延遲載入。這樣會打敗預先載入掃描器,並造成不必要的延遲。

視圖片大小而定,這可能會是最大內容繪製 (LCP) 的候選元素。如果預先載入掃描器無法事先推測擷取圖片資源(可能是網頁的樣式表封鎖而無法轉譯),可能就會導致 LCP 受到影響。

做法就是變更圖片標記:

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

對於啟動時位於可視區域中的圖片,這是最適合的模式,因為預先載入掃描器會更快找到並擷取圖片資源。

WebPageTest 網路瀑布圖呈現啟動期間,可視區域中的圖片載入情境。圖片並未延後載入,也就是說,它與載入的指令碼無關,因此預先載入掃描器可以更快發現圖片。
圖 9:在行動裝置上的 Chrome 透過模擬 3G 連線執行的 WebPageTest 網路瀑布圖。預先載入掃描器會在 CSS 和 JavaScript 開始載入前找到圖片資源,讓瀏覽器有頭考慮載入圖片。

這個簡化的範例就是在連線速度緩慢時,LCP 提升了 100 毫秒。這看起來也許沒有什麼進步,但當您認為此解決方案可以快速解決標記問題,而且大部分網頁比這些範例還複雜。也就是說,LCP 候選項目可能需要運用其他資源處理頻寬,因此這類最佳化工作變得越來越重要。

CSS 背景圖片

請注意,瀏覽器預先載入掃描器掃描「標記」。但不會掃描其他資源類型 (例如 CSS),這類項目可能會擷取 background-image 屬性所參照圖片的擷取作業。

瀏覽器和 HTML 一樣,會將 CSS 處理成專屬的物件模型,稱為 CSSOM。如果在建構 CSSOM 時發現外部資源,這些資源會在探索當下要求這些資源,而非預先載入掃描器。

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

這張 WebPageTest 網路瀑布圖呈現一個網頁,其中含有使用 background-image 屬性從 CSS 載入的 LCP 候選網頁。由於 LCP 候選圖片屬於瀏覽器預先載入掃描器無法檢查的資源類型,因此資源會延遲載入,直到 CSS 完成下載和處理才開始,這會延遲 LCP 候選圖片的繪製時間。
圖 10:一個 WebPageTest 網路瀑布圖在行動裝置透過 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 提示很小,但可協助瀏覽器更快找到圖片。

顯示 CSS 背景圖片 (也就是 LCP 候選圖片) 但因為使用 rel=preload 提示而更快載入的 WebPageTest 網路瀑布圖。LCP 時間大約能縮短 250 毫秒。
圖 11:一個 WebPageTest 網路瀑布圖在行動裝置透過 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 網路瀑布圖,顯示在 Chrome 行動裝置上執行的網頁。網頁的 LCP 候選圖片是從 <img> 元素載入,但預先載入掃描工具找到的圖片,因為在個別資源中載入網頁所需的 CSS 和字型,並不會延遲預先載入掃描器執行工作。

如果 CSS 所有字型都以 Base64 資源內嵌,會發生什麼事?

一個 WebPageTest 網路刊登序列圖表,內含外部 CSS 檔案,其中參照四個字型。預先載入掃描器發現 LCP 圖片無法延遲。
圖 13:一個 WebPageTest 網路瀑布圖,顯示在 Chrome 行動裝置上執行的網頁。網頁的 LCP 候選項目是從 <img> 元素載入的圖片,但「」」中 CSS 及其四個字型資源的嵌入會延遲預先載入掃描器探索圖片,直到這些資源完全下載完成。

在此範例中,內嵌的影響會對 LCP 造成負面影響,一般而言則產生負面影響。LCP 圖片所在網頁版本大約會在 3.5 秒內繪製至未內嵌任何內容。內嵌所有內容的網頁在超過 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。在這些範例中,LCP 圖片的要求相較於不需要 JavaScript 顯示的同等伺服器轉譯體驗,會大幅延遲。

本文將稍微說明本文重點,但對用戶端轉譯標記的影響遠遠不僅止於擊敗預先載入掃描器。舉例來說,導入 JavaScript 來加強使用體驗,且不會產生不必要的處理時間,進而影響與下一次繪製互動 (INP)

此外,與伺服器傳送的標記數量相同,在用戶端顯示極大量的標記更有可能產生「長篇工作」。如此一來,除了 JavaScript 所涉及的額外處理外,瀏覽器從伺服器串流標記,並以可避免長時間工作的方式分割算繪。另一方面,用戶端算繪的標記會以單一單體式工作的形式處理,因此除了 INP 以外,網頁回應時間指標 (例如總封鎖時間 (TBT)首次輸入延遲時間 (FID)) 可能會受到影響。

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

如果您的網頁「需要」使用 JavaScript 將功能附加至網頁標記的某些部分,您仍可使用 SSR 來採用基本 JavaScript,或是採用水分,才能充分發揮這兩種功能。

協助預先載入掃描器

預先載入掃描器是一種非常有效的瀏覽器最佳化工具,可在啟動期間加快網頁載入速度。只要避免使用會妨礙事先探索重要資源的模式,您不僅能夠簡化開發程序,還能創造更優質的使用者體驗,進而產生更好的許多指標,包括一些網站體驗指標

回顧一下,本文提供以下要點:

  • 瀏覽器預先載入掃描器是一種次要 HTML 剖析器,如果在遭到封鎖的情況下有機會更快擷取資源,就會在主要 HTML 剖析器前先行掃描。
  • 預先載入掃描器無法找到在初始導覽要求中,伺服器提供的標記中未列出的資源。預先載入掃描器的失準方式包括但不限於:
    • 使用 JavaScript 將資源插入 DOM,例如指令碼、圖片、樣式表,或是從伺服器初始標記酬載中可套用的任何其他項目。
    • 使用 JavaScript 解決方案延遲載入不需捲動位置的圖片或 iframe。
    • 在用戶端上轉譯標記,其中可能包含使用 JavaScript 的文件子資源參照。
  • 預先載入掃描器只會掃描 HTML。並不會檢查其他資源 (尤其是 CSS) 的內容,其中可能包含重要資產 (包括 LCP 候選項目) 的內容。

無論原因為何,如果您「無法」避免出現會對預先載入掃描器效能造成負面影響的模式,請考慮使用 rel=preload 資源提示。如果您「確實」使用 rel=preload,請在研究室工具中進行測試,確保這的效果符合預期。最後,不要預先載入過多資源,因為如果決定所有首要因素,就不會有任何結果。

資源

主頁橫幅來自 Unsplash 網站上 (由 Mohammad Rahmani 提供)。