博客 / 詳情

返回

⚡️ [性能優化] 瀏覽器是如何用 HTML Preload Scanner 偷偷優化資源下載的

如果你喜歡我的文章,希望點贊👍 收藏 📁 評論 💬 三連支持一下,謝謝你,這對我真的很重要!

大家好,我是專注於做性能優化的鹵代烴。

做網頁相關的性能優化時,需要對瀏覽器的底層原理有一定的瞭解,這樣才能更好的讓頁面走在 happy path 上。今天我們就瞭解一個很少被人所知的瀏覽器默認性能優化方案 —— HTML Preload Scanner,看看它是如何優化網絡資源加載速度的。

瀏覽器如何解析 CSS/JS 資源

我們簡單回顧一下瀏覽器是如何「解析」 HTML/CSS/JS/Image 等主要資源的。

HTML 作為網頁入口,肯定是第一個下載的,下載後就會進入一行一行解析環節。解析到子資源的時候,就會存在這麼幾種經典情況:

1. 如果是 CSS 資源,就要停止 HTML 的解析和渲染,得等到 CSS 解析 & 執行完畢再解析渲染 HTML。

為什麼要這樣做?假設加載一行 CSS 就渲染一次頁面,如果我們有這樣的 CSS 文件:

/* first rule */
p {
  font-size: 12px;
}

/* ... */

/* last rule */
p {
  font-size: 20px;
}

解析執行第一條 CSS 規則,立馬渲染,所有的字體都改成了 12px;過了 100ms,解析執行到最後一條規則,立馬渲染,所有字體變成 20px。

先不説解析後就渲染的效率性能問題,這種 100ms 內字體變大變小的閃爍問題會帶來極其糟糕的用户體驗(這個官方説法叫 FOUC)。

瀏覽器為了一定程度內解決這個問題,採用的方案就是解析 CSS 資源時,停止 HTML 後面內容的解析和渲染,等這個 CSS 資源全部解析完畢後再一併渲染。


2. 如果是沒有加 async/defer 的 JS 資源,也得等 JS 解析 & 執行完畢後再解析渲染 HTML。

這個思路和 CSS 類似,因為這種 JS 資源可能會操作 DOM,所以也得等待 JS 解析執行完畢,這也是為什麼有條性能優化規則是「把 JS 文件放在 Body 最後面,防止阻塞渲染」。


從上面可以看出,渲染這個事兒,遇到 CSS 和 JS 只能等,但是有一個事情其實不用等,那就是子資源的「下載」,如果你有留意的話,上面的內容我討論的都是「解析 + 渲染」,「下載」隻字未提。

這個現象瀏覽器工程師們早早就發現了,雖然一些「解析 + 渲染」行為是串行的,但是「下載」不是啊!瀏覽器可以提前下載 HTML 裏能下載的內容,然後需要「解析」時可以直接拿來就用,這樣就能大大提升首屏性能指標了。

這個性能優化方案就叫 HTML Preload Scanner,Webkit 在 2008 年就引入這個技術了,可以認為這是現代瀏覽器的標配功能了。

HTML Preload Scanner

接下來我們詳細説一下這個功能是如何運作的。

瀏覽器解析 HTML 的時候,會有兩個解析器:

  • 一個是正式的,叫 Primary HTML Parser,會一行一行解析運行,遇到阻塞性資源就會停止 HTML 的解析,直至阻塞性資源加載運行完畢
  • 另一個叫 HTML Preload Scanner,會直接收集當前 HTML 裏所有值得下載的子資源,然後並行下載它們

Primary HTML Parser and HTML Preload Scanner

兩個 HTML Parser 互相配合,就能從底層上優化網頁首屏性能,帶來更好的用户體驗。

如何觀測 HTML Preload Scanner 是否正常工作呢?目前各大瀏覽器並沒有暴露此標識,作為 Web 開發者其實可以根據 Perfs 火焰圖來判斷。

如果一個網頁命中 Pre-Scan,那麼一個典型特徵是,JS/CSS/Img 等首屏子資源會在 HTML 請求結束後同時發起請求

html-preload-scanner-pic

如果火焰圖有上面類似的特徵,一般就是命中了。

利用 Preload Scanner 提升性能

因為 HTML Preload Scanner 是個瀏覽器默認的功能,所以不用考慮兼容性問題。但又因為它太基礎太底層了,很多人不知道這個功能,所以很難優化意識,本小結就提供一些優化建議,輔助大家使用。

少用 JS 動態加載腳本

HTML Preload Scanner,正如名字,它是一個 HTML Parser,不負責解析 JS。所以説,如果 HTML 裏面有些類似的內聯 JS 腳本:

<script>
const scriptEl = document.createElement('script');
scriptEl.src = 'test.js';
document.body.appendChild(scriptEl);
</script>

這種內聯 JS 腳本會被 HTML Preload Scanner 直接跳過不解析的,這就會導致 test.js 不能被提前發現並下載,只能等 Primary HTML Parser 執行到對應位置觸發 下載/解析/運行 的連招。

從上面思路出發,我們也可以想到,如果一個網頁是純 SPA 頁面,所有的後續資源都依賴 main.js 加載,其實在資源加載和渲染上都會有劣勢的,改造為 SSR 頁面會有更好的性能表現(就是吃服務器資源)。

少用 CSS 加載資源

同樣的道理,HTML Preload Scanner 也會跳過 HTML 中的內聯 CSS 內容的解析。

<style>
.lcp_img {
  background-image: url("demo.png");
}
</style>

對於上面的 CSS 來説,HTML Preload Scanner 直接跳過不解析的,只能等 Primary HTML Parser 執行到這段 CSS 位置,解析才會觸發 demo.png 的下載,時機的後延會帶來一定的劣化。

靈活使用 Preload

業務代碼千奇百怪,可能有些資源文件就得在 JS & CSS 裏寫。如果這些資源不重要,其實就不用管,如果這些資源比較重要,我們可以用曲線救國的方式,利用 <link rel="preload" /> 標籤來預加載資源:

<link rel="preload" href="demo.png" as="image" type="image/png" />
<style>
.lcp_img {
  background-image: url("demo.png");
}
</style>

對於上面的案例,HTML Preload Scanner 雖然會跳過內聯 CSS 的資源,但是會下載 preload 標記的預加載資源。

當然,這種方案的問題是,preload 會提升相關資源的優先級。在網速這個上限的制約下,可能會擠壓其他資源的帶寬,所以此方案需要做一些定量的性能分析,防止引發劣化。

不用 HTML CSP meta 標籤

部分網頁出於安全要求,會在 HTML 里加入 CSP meta tag,以防止一些 XSS 攻擊。

<head>
  <meta http-equiv="Content-Security-Policy" content="default-src 'self';">
</head>

這種安全策略在業務角度上沒什麼問題,但是會徹底破壞 HTML Preload Scanner 的優化。

在 chromium 的源碼中,有這麼一段邏輯:

// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/parser/html_preload_scanner.cc;l=1275;bpv=0;bpt=1

// Don't preload anything if a CSP meta tag is found. We should rarely find
// them here because the HTMLPreloadScanner is only used for the synchronous
// parsing path.
if (seen_csp_meta_tag) {
  // Reset the tokenizer, to avoid re-scanning tokens that we are about to
  // start parsing.
  source_.Clear();
  tokenizer_->Reset();
  return pending_data;
}

也就是説,一旦解析到 CSP 相關的 meta 標籤,就會清理所有 Preload 的內容,Preload Scanner 徹底失效。

那麼如果我們有 CSP 要求,又不想破壞 HTML Preload Scanner 優化,應該怎麼做呢?這個方案就是使用 HTTP CSP Header


我本地做了幾個測試,首先是一個沒有任何 CSP 內容的網頁,我們可以看到 Preload Scanner 是正常工作的:

normal


加上 HTML CSP meta Tag,並行下載全都變成了串行加載,首屏性能大大劣化:

<head>
  <meta http-equiv="Content-Security-Policy" content="default-src 'self';">
</head>

html-csp-meta-tag


換成 HTTP CSP Header,Preload Scanner 還是正常工作的:

Content-Security-Policy: default-src 'self'

http-csp-header


所以我們可以通過 HTTP CSP Header 的方案替換 HTML CSP Meta Tag,防止 Pre-Scan 失效。

總結

編寫 HTML Preload Scanner 友好的前端代碼,可以讓瀏覽器的資源加載走在 happy path 上,優化 Web 的整體資源加載性能。

參考內容

  • web.dev: Don't fight the browser preload scanner
  • stackoverflow: Content-Security-Policy meta tag preventing preload scanner to work properly in Chrome
  • MDN: Content Security Policy (CSP)
  • chromium: html_preload_scanner.cc

如果你喜歡我的文章,希望點贊👍 收藏 📁 評論 💬 三連支持一下,謝謝你,這對我真的很重要!

歡迎大家關注我的微信公眾號:滷蛋實驗室,目前專注前端技術,對圖形學也有一些微小研究。

原文鏈接 👉 ⚡️ [性能優化] 瀏覽器是如何用 HTML Preload Scanner 偷偷優化資源下載的:更新更及時,閲讀體驗更佳

user avatar buxia97 頭像 gaoming13 頭像 waweb 頭像 iymxpc3k 頭像 huanjinliu 頭像 nihaojob 頭像 warn 頭像 user_ze46ouik 頭像 cipchk 頭像 lihaixing 頭像 caideheirenyagao 頭像 liujunqi 頭像
16 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.