使用 Performance 看看瀏覽器在做些什么
作者:ES2049
來源:https://segmentfault.com/a/1190000038442806
前言
Chrome 瀏覽器的 Performance 面板為我們提供了檢測頁面性能的能力,但其提供的遠不止一些性能數(shù)據。本文將從工作原理的視角,結合實際工程的錄制結果,探一探性能面板向我們透露的其他信息。
性能面板
關于面板的功能與使用方法,可以參考這篇文章。本節(jié)主要介紹瀏覽器架構與性能面板的關系。
因為尚未決出最終的標準架構,各大瀏覽器的實現(xiàn)細節(jié)各有不同。這里我們以 Chrome 的架構為例,對照其架構與性能面板的關系。
由下圖我們可以看到性能面板呈現(xiàn)的幾個主要線程。性能面板并不包含架構中全部的線程,主要還是與頁面渲染過程相關的部分。
Network
Network 代表瀏覽器進程中的網絡線程,我們可以看到時間軸上包含了所有的網絡請求和文件下載的調用信息,并以不同顏色標識不同類型的資源。
Main
Main 代表渲染進程中的主線程,渲染相關的事情基本都是它來做,腳本執(zhí)行、樣式計算、布局計算、繪制等等。
Compositor & Raster
Compositor 代表渲染進程中的合成線程,Raster 代表渲染進程中的柵格線程。如今瀏覽器繪制一個頁面,可以分為以下幾步:
主線程將頁面分成若干圖層(后文中會提及 Update Layer Tree) 柵格線程分別對每一個層進行柵格化處理 合成線程將柵格化的圖塊合并成一個頁面

我們可以看到,在性能面板中主線程在最后調用了柵格線程做實際的渲染。
GPU
顯然,這部分就是 GPU Process 中的 GPU 線程。
瀏覽器的工作報告
接下來我們將大致從時間維度,看看瀏覽器錄制下來的「工作報告」。
文檔的下載解析
我們旅途的起點將從點擊 Chrome Performance Panel 的 Reload 按鈕(形如刷新)開始。當前頁面首先進行卸載,伴隨著幾個日志上報,瀏覽器開始了 index.html 的下載工作。
HTML 文檔下載完成后,瀏覽器開始按照 HTML 標準對 index.html 進行解析,在主線程中將接收到的文本字符串解析為 DOM 。我們可以注意到,HTML 的解析過程并不是一氣呵成,這是因為 HTML 通常還包括了其他外部資源,如圖片、CSS、JS 等。這些文件需要通過網絡請求或緩存來獲取。其中,當 HTML 解析器解析到 <script> 標簽時,HTML 文檔的解析過程就會中止,轉而去加載、解析和執(zhí)行腳本。因此,從主線程的時間軸可以看出,Parse HTML 的過程是斷斷續(xù)續(xù)的。
不同資源的處理
以下處理策略都可以在主線程中看到,但是不同資源的處理條長短差距較大,截圖困難,這里不做呈現(xiàn)。
那么瀏覽器對不同資源的處理策略是怎樣的呢?
瀏覽器下載 HTML 并解析,如果遇到外部 CSS 等資源,就會由 Browser 進程中的 network 線程下載 當 CSS 下載時,HTML 的解析過程可以繼續(xù) 當解析遇到了外部 Script 標簽(不包含 async、defer 屬性)時,解析停止,直到腳本下載并執(zhí)行完成
總的來說,瀏覽器對 HTML 的解析過程不會被 CSS、IMG 等資源的下載阻塞,但腳本的加載和執(zhí)行會終止 HTML 的解析。這主要是因為 JS 可能會改變 DOM 的結構,或者是 JS 動態(tài)加載其他 JS 再改變 DOM 等潛在問題。
顯然,盡管瀏覽器可以并發(fā)幾個 network 線程下載資源,但如果僅像上述策略這樣處理,當解析到 <script> 時,如果文件較大或者延遲較高,可能會發(fā)生「腳本獨占線程而沒有其他資源在下載」的空窗期(idle network)。因此,pre-loader (或者 preload scanner 等叫法)將會在主線程之外,掃描余下的標簽,充分利用 network 線程下載其他資源。這種機制可以優(yōu)化 19% 的加載時長。
腳本的解析執(zhí)行
對于重業(yè)務邏輯的復雜中后臺應用而言,腳本帶來的性能開銷,往往是占主要地位的。我們從下圖的例子就可以看出,去除 beforeunload 之前的卸載,腳本本身的時間開銷占比已過半。解析 HTML 在其次,至于其他樣式計算、微任務、垃圾回收等等,倒不是最痛的地方。當然,該例子工程本身重業(yè)務邏輯,JavaScript 代碼量決定著其高成本。
有時我們可以考慮使用 async 或者 defer 屬性來提高頁面性能,二者的差異不再贅述。需要專門說明的是動態(tài)添加腳本的情況。如下面示例代碼所示,腳本被 append 到文檔中后就會開始下載,并且默認和 async 具有一樣的行為,即「先加載完的先執(zhí)行」。
let script = document.createElement('script');
script.src = "/xxx/a.js";
document.body.append(script);
如果專門設置了 async 屬性,則會按照 defer 的行為來,即「先加載到的先執(zhí)行」。
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false;
document.body.append(script);
}
// 因為 async = false,所以按順序先執(zhí)行 big;否則(一般會先)執(zhí)行 small
loadScript("/xxx/big.js");
loadScript("/xxx/small.js");
從下圖中可以看到,調用棧中執(zhí)行的 appendChild 方法動態(tài)添加了 script 腳本,之后很快開始了下載動作。動態(tài)加載的腳本完成下載后,又第一時間開始了腳本執(zhí)行。

lifecycle 和 paint timing
下圖展示的是文章中提及的頁面生命周期流程圖。本節(jié)我們結合 Performance,對照該圖進行觀察。
beforeunload
因為 Performance 的錄制是在已有頁面上進行 reload,所以記錄的生命周期從頁面的卸載開始。如下圖 Main 所示,beforeunload 事件首先被瀏覽器觸發(fā)。可以注意到,黃色條 Event: beforeunload 是瀏覽器自身觸發(fā)的活動,我們稱之為根活動(Root activities)。
pagehide
從下圖中我們可以注意到,為什么事件的觸發(fā)順序和上面的生命周期流程圖不一致,是 pagehide -> visibilitychange -> unload 呢?事實上,在瀏覽器之前的設計中,如果頁面在卸載階段可視,visibilitychange 就會在 pagehide 之后觸發(fā),正如下圖截圖中一樣。這就使得頁面的卸載在不同可視情況下,有著不一致的生命周期與事件順序,給開發(fā)者帶來復雜性。
在未來新版本瀏覽器中,卸載階段的事件順序會進行統(tǒng)一,目前進度在這一 issue 下。也正因為這部分的調整,unload 已經不建議在代碼實現(xiàn)中使用了。
first paint
首先區(qū)分下以下兩個時間點:
first paint:指的是首個像素開始繪制到屏幕上的時機,例如一個頁面的背景色 first contentful paint:指的是開始繪制內容的時機,如文字或圖片

從 Performance 中,我們可以看出首次繪制的一系列動作(有些過程啪的一下很快啊,截圖就省了):
CSS 加載完成 Parse Stylesheet:解析樣式表,構建出 CSSOM Recalculate Style:重新計算樣式,確定樣式規(guī)則 Layout:根據計算結果進行布局,確定元素的大小和位置 Update Layer Tree:更新渲染層樹 Paint:根據 Layer Tree 繪制頁面(位置、大小、顏色、邊框、陰影等) Composite Layers:組合層,瀏覽器將圖層合并后輸出到屏幕
Layout 之后的過程很快,這里放大些倍數(shù)來查看:
DOMContentLoaded
DOMContentLoaded 表示 HTML 已經完全被加載和解析,當然樣式表、圖片等資源還不一定已經完成加載。從下圖中可以看到,經過多段 HTML 解析后,DCL 之后就沒有其他的 Parse HTML 了。
pageshow/load
因導航而使得瀏覽器在窗口內呈現(xiàn)文檔時,瀏覽器會在 window 上觸發(fā) pageshow 事件,具體的時機可參考這里。不僅如此,當頁面是初次加載時,pageshow 事件會在 load 事件后觸發(fā)。
那么回到 Performance 的時間軸,從下圖我們可以看到,在紅色虛線(標志著 load)之后,瀏覽器觸發(fā)了 pageshow 事件,也就是上文提及的根活動。
任務與性能問題
比較可惜的是,Performance 還無法清晰的看出 Event Loop。下圖中灰色的 Task 并不是指宏任務,其代表的是「當前主線程忙碌,無法響應用戶交互」;Run Microtasks 則確實是在一次任務的末尾執(zhí)行的微任務。當我們點開調用棧觀察時,可以看到源碼中的回調函數(shù)以及對應的源碼位置。
通過 Task 可以定位性能出現(xiàn)問題的地方。RAIL 模型告訴我們需要重點關注占用 CPU 超出 50ms 的復雜任務,以提供連貫的交互體驗。當然,這里更多的是對交互階段的響應的要求,而不一定是對初始加載階段的要求。
總結
本文從工作原理的視角,結合實際工程的錄制結果,進行了一次實踐對理論知識的檢驗。Performance 不僅是性能分析工具,還是探究瀏覽器工作原理的小霸王學習機。總的來說,瀏覽器的工作是充實且復雜的,與我們打工人的摸魚日常形成了對比,還是需要進一步加深學習與思考呀。
參考鏈接
[1] Measure performance with the RAIL model
[2] Get Started With Analyzing Runtime Performance
[3] Inside look at modern web browser
[4] JavaScript Start-up Performance
[5] How browsers work
[6] How the Browser Pre-loader Makes Pages Load Faster
最后
歡迎加我微信(winty230),拉你進技術群,長期交流學習...
歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...



