如果你喜歡我的文章,希望點贊👍 收藏 📁 評論 💬 三連支持一下,謝謝你,這對我真的很重要!
大家好,我是專注於做性能優化的鹵代烴。
做網頁相關的性能優化時,需要對瀏覽器的底層原理有一定的瞭解,這樣才能更好的讓頁面走在 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 裏所有值得下載的子資源,然後並行下載它們
兩個 HTML Parser 互相配合,就能從底層上優化網頁首屏性能,帶來更好的用户體驗。
如何觀測 HTML Preload Scanner 是否正常工作呢?目前各大瀏覽器並沒有暴露此標識,作為 Web 開發者其實可以根據 Perfs 火焰圖來判斷。
如果一個網頁命中 Pre-Scan,那麼一個典型特徵是,JS/CSS/Img 等首屏子資源會在 HTML 請求結束後同時發起請求:
如果火焰圖有上面類似的特徵,一般就是命中了。
利用 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 是正常工作的:
加上 HTML CSP meta Tag,並行下載全都變成了串行加載,首屏性能大大劣化:
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
</head>
換成 HTTP CSP Header,Preload Scanner 還是正常工作的:
Content-Security-Policy: default-src 'self'
所以我們可以通過 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 偷偷優化資源下載的:更新更及時,閲讀體驗更佳