課外加餐:5 | 效能分析工具:如何分析Performance中的Main指標?
前言:該篇說明:請見 說明 —— 瀏覽器工作原理與實踐 目錄
上節介紹瞭如何使用 Performance,而且還提到了效能指標面板中的 Main 指標,它詳細的記錄了渲染主執行緒上的任務執行記錄,通過分析 Main 指標,就能夠定位到頁面中所存在的效能問題,本節就來介紹如何分析 Main 指標。
任務 VS 過程
開始前先講清楚兩個概念,那就是 Main 指標中的任務和過程,在《15 | 訊息佇列和事件迴圈:頁面是怎麼活起來的?》和 《加餐二 | 任務排程:有了 setTimeout ,為什麼還要使用 rAF?》這兩節中分析過,渲染程序中維護了訊息佇列,如果通過 SetTimeout 設定的回撥函式,通過滑鼠點選的訊息事件,都會以任務的形式新增訊息佇列中,然後任務排程器會按照一定規則從訊息佇列中取出合適的任務,並讓其在渲染主執行緒中執行。
而今天所分析的 Main 指標就記錄渲染主執行緒上所執行的全部任務,以及每個任務的詳細執行過程。
開啟 Chrome 的開發者工具,選擇 Performance 標籤,然後記錄載入階段任務執行記錄,然後關注 Main 指標,如下圖所示:
任務和過程
觀察上圖,圖上方有很多一段一段灰色橫條,每個灰色橫條就對應了一個任務,灰色長條的長度對應了任務的執行時長。通常,渲染主執行緒上的任務都是比較複雜的,如果只單純記錄任務執行的時長,那麼依然很難定位問題,因此,還需要將任務執行過程中的一些關鍵的細節記錄下來,這些細節就是任務的過程,灰線下面的橫條就是一個個過程,同樣這些橫條的長度就代表這些過程執行的時長。
直觀地理解,你可以把任務看成是一個 Task 函式,在執行 Task 函式的過程中,它會呼叫一系列的子函式,這些子函式就是所提到的過程。為了很好地理解,我們來分析下面這個任務的圖形:
單個任務
觀察上面這個任務記錄的圖形,可以把該圖形看成是下面 Task 函式的執行過程:
function A() { A1() A2() } function Task() { A() B() } Task()
結合程式碼和上面的圖形,可以得出以下資訊:
- Task 任務會首先呼叫 A 過程;
- 隨後 A 過程又依次呼叫了 A1 和 A2 過程,然後 A 過程執行完畢;
- 隨後 Task 任務又執行了 B 過程;
- B 過程執行結束,Task 任務執行完成;
- 從圖中可以看出,A過程執行時間最長,所以在 A1 過程中,拉長了整個任務的執行時長。
分析頁面載入過程
通過以上介紹,相信你已經掌握瞭如何解讀 Main 指標中的任務了,那麼接下來就可以結合 Main 指標來分析頁面的載入過程。先來分析一個簡單的頁面,程式碼如下:
<html> <head> <title>Main</title> <style> area { border: 2px ridge; } box { background-color: rgba(106, 24, 238, 0.26) height: 5em; margin: 1em; width: 5em; } </style> </head> <body> <div class="area"> <div class="box rAF"></div> </div> <br> <script> function setNewArea() { let el = document.createElement('div') el.setAttribute('class', 'area') el.innerHTML = '<div class="box rAF"></div>' document.body.append(el) } setNewArea() </script> </body> </html>
觀察這段程式碼可以看出,它只是包含了一段 CSS 樣式和一段 JS 內嵌程式碼,其中在 JS 中還執行了 DOM 操作了,我們就結合這段程式碼來分析頁面的載入流程。
首先生成報告頁,再觀察報告頁中的 Main 指標,由於閱讀實際指標比較費勁,所以我手動繪製了一些關鍵的任務和其執行過程,如下圖所示:
Main 指標
通過上面的圖形可以看出,載入過程主要分為三個階段,它們分別是:、
- 導航階段,該階段主要是從網路程序接收 HTML 響應頭和 HTML 響應體。
- 解析 HTML 資料階段,該階段主要是將接收到的 HTML 資料轉換為 DOM 和 CSSOM。
- 生成可顯示的點陣圖階段,該階段主要是利用 DOM 和 CSSOM ,經過計算佈局、生成層樹(LayerTree)、生成繪製列表(Paint)、完成合成等操作,生成最終的圖片。
那麼接下來就按照這三個步驟來介紹如何解讀 Main 指標上的資料。
導航階段
在分析這個階段之前,先簡要地回顧下導航流程,大致的流程是這樣的:
當點選了 Performance 上的重新錄製按鈕之後,瀏覽器程序會通知網路程序去請求對應的 URL 資源;一旦網路程序從伺服器接收到 URL 的響應頭,便立即判斷該響應頭中的 content-type 欄位是否屬於 text/html 型別;如果是,那麼瀏覽器程序會讓當前的頁面執行退出前的清理操作,比如執行 JS 中的 beforunload 事件,清理操作執行結束之後就準備顯示新頁面了,這包括解析、佈局、合成、顯示等一系列操作。
因此,在導航階段,這些任務實際上是在老頁面的渲染主執行緒上執行的。如果想要了解導航流程的詳細細節可以回顧下《04 | 導航流程:從輸入 URL 到頁面展示,這中間發生了什麼?》這篇文章,在這篇文中有介紹導航流程,而導航階段和導航流程又有著密切的關聯。
回顧了導航流程之後,接著來分析第一個階段的任務圖形,為了讓你更加清晰觀察上圖中的導航階段,我將其放大了,最終效果如下圖所示:
請求 HTML 資料階段
觀察上圖,如果你熟悉了導航流程,那麼就很容易根據圖形分析出這些任務的執行流程了。
具體地講,當點選重新載入按鈕後,當前的頁面會執行上圖中的這個任務:
- 該任務的第一個子過程就是 Send request,該過程表示網路請求已被髮送。然後該任務進入了等待狀態。
- 接著由網路程序負責下載資源,當接收到響應頭的時候,該任務便執行 Receive Response 過程,該過程表示接收到 HTTP 的響應頭了。
- 接著執行 DOM 事件:pageIndex、visibilitychange 和 unload 等事件,如果註冊了這些事件的回撥函式,那麼這些回撥函式會依次在該任務中被呼叫。
- 這些事件被處理完成之後,那麼接下來就接收 HTML 資料了,這體現在了 Receive Data 過程,Receive Data 過程表示請求的資料已被接收,如果 HTML 資料過多,會存在多個 Receive Data 過程。
等到所有的資料都接收完成之後,渲染程序會觸發另外一個任務,該任務主要執行 Finish load 過程,該過程表示網路請求已經完成。
解析 HTML 資料階段
好了,導航階段結束之後,就進入到了解析 HTML 資料階段了,這個階段的主要任務就是通過解析 HTML 資料、解析 CSS 資料、執行 JS 來生成 DOM 和 CSSOM。那麼下面繼續來分析這個階段的圖形,看看它到底是怎麼執行的?同樣,我也放大了這個階段的圖形,觀看下圖:
解析 HTML 資料階段
觀察上圖這個圖形可以看出,其中一個主要的過程是 HTMLParser,顧名思義,這個過程是用來解析 HTML 檔案,解析的就是上個階段接收到的 HTML 資料。
- 在 ParserHTML 的過程中,如果解析到了 script 標籤,那麼便進入了指令碼執行過程,也就是圖中的 Evalute Script。
- 我們知道,要執行一段指令碼需要首先編譯該指令碼,於是在 Evalute Script 過程中,先進入了指令碼編譯過程,也就是圖中的 Complie Script。指令碼編譯好之後,就進入程式執行過程,執行全域性程式碼時,V8 會先構造一個 anonymous 過程,在執行 anonymous 過程中,會呼叫 setNewArea 過程,setNewArea 過程中又呼叫了 createElement,由於之後呼叫了 document.append 方法,該方法會觸發 DOM 內容的修改,所以又強制執行了 ParserHTML 過程生成新的 DOM。
- DOM 生成完成之後,會觸發相關的 DOM 事件,比如典型的 DOMContentLoaded,還有 readyStateChanged。
DOM 生成之後,ParserHTML 過程繼續計算樣式表,也就是 Reculate Style,這就是生成 CSSOM 的過程,關於 Reculate Style 過程,你可以參考《05 | 渲染流程(上):HTML、CSS 和 JS,是如何變成頁面的?》的內容,倒了這裡一個完整的 ParserHTML 任務就執行結束了。
生成可顯示點陣圖階段
生成了 DOM 和 CSSOM 之後,就進入了第三個階段:生成頁面上的點陣圖。通常這需要經歷佈局(Layout)、分層、繪製、合成 等一系列操作,同樣,我將第三個階段的流程也放大了,如下圖所示:
生成可顯示的點陣圖
結合上圖可以發現,在生成完了 DOM 和 CSSOM 之後,渲染主執行緒首先執行了一些 DOM 事件,諸如 readyStateChange、load、pageshow。具體地講,如果使用 JS 監聽了這些事件,那麼這些監聽的函式會被渲染主執行緒依次呼叫。
接下來就正式進入顯示流程了,大致過程如下所示:
- 首先執行佈局,這個過程對應圖中的 Layout。
- 然後更新層樹(LayerTree),這個過程對應圖中的 Update LayerTree。
- 有了層樹之後,就需要為層樹中的每一層準備繪製列表了,這個過程就稱為 Paint。
- 準備每層的繪製列表之後,就需要利用繪製列表來生成相應圖層的點陣圖了,這個過程對應圖中的 Composite Layers。
走到了 Composite Layers 這步,主執行緒的任務就完成了,接下來主執行緒會將合成的任務完成教給合成執行緒來執行,下面是具體的過程,你也可以對照著 Composite、Raster 和 GPU 這三個指標來分析,參考下圖:
顯示流程
結合渲染流水線和上圖,再來梳理下最終影象是怎麼顯示出來的。
- 首先主執行緒執行到 Composite Layers 過程之後,便會將繪製列表等資訊提交給合成執行緒,合成執行緒的執行記錄可以通過 Compositor 指標來檢視。
- 合成執行緒維護了一個 Raster 執行緒池,執行緒池中的每個執行緒稱為 Rasterize,用來執行光柵化操作,對應的任務就是 Rasterize Paint。
- 當然光柵化操作並不是在 Rasterize 執行緒中直接執行的,而是在 GPU 程序中執行的,因此 Rasterize 執行緒需要和 GPU 執行緒保持通訊。
- 然後 GPU 生成影象,最終這些圖層會被提交給瀏覽器程序,瀏覽器程序將其合成並最終顯示在頁面上。
通用分析流程
通過對 Main 指標的分析,我們把導航流程,解析流程和最終的顯示流程都串起來了,通過 Main 指標的分析,我們把頁面的載入過程執行流程又有了新的認識,雖然實際情況比這個複雜,但是萬變不離其宗,所有的流程都是圍繞這條線來展開的,也就是說,先經歷導航階段,然後經歷 HTML 解析,最後生成最終的頁面。
總結
本文主要的目的是讓我們學會如何分析 Main 指標。通過頁面載入過程的分析,就能掌握一套標準的分析 Main 指標的方法,在該方法中,將載入過程劃分為三個階段:
- 導航階段;
- 解析 HTML 檔案階段;
- 生成點陣圖階段。
在導航流程中,主要是處理響應頭的資料,並執行一些老頁面退出之前的清理操作。在解析 HTML 資料階段,主要是解析 HTML 資料、解析 CSS 資料、執行 JS 來生成 DOM 和 CSSOM。最後在生成最終顯示點陣圖的階段,主要是將生成的 DOM 和 CSSOM 合併,這包括了佈局(Layout)、分層、繪製、合成等一系列操作。
通過 Main 指標,完整地分析了一個頁面從載入到顯示的過程,瞭解這個流程,自然就會去分析頁面的效能瓶頸,比如可以通過 Main 指標來分析 JS 是否執行時間過久,或者通過 Main 指標分析程式碼裡面是否存在強制同步佈局等操作,分析出來這些原因之後,可以有針對性地去優化我們的程式。
思考題
在《18 | 巨集任務和微任務:不是所有任務都是一個待遇》這節中介紹微任務時,我們提到過,在一個任務的執行過程中,會在一些特定的時間點來檢查是否有微任務需要執行,我們把這些特定的檢查時間點稱為檢查點。瞭解了檢查點之後,可以通過 Performance 的 Main 指標來分析下面這兩段程式碼:
<body> <script> let p = new Promise(function (resolve, reject)) { resolve('成功!') }); p.then(function (successMessage) { console.log('p! ' + successMessage); }) let p1 = new Promise(fucntion (resolve, reject) { resolve('成功!'); }); p1.then(function (successMessage) { console.log('p1!' + successMessage); }) </script> </body>
第一段程式碼
<body> <script> let p = new Promise(function (resolve, reject)) { resolve('成功!'); }); p.then(function (successMessage) { console.log('p! ' + successMessage); }) </script> <script> let p1 = new Promise(function (resolve, reject) { resolve('成功!') }); p1.then(function (successMessage) { console.log('p1! ' + successMessage); }) </script> </body>
第二段程式碼
今天的任務是結合 Main 指標來分析上面這兩段程式碼中微任務執行的時間點有何不同,並給出分析結果和原因。