從 B 站出發,用 Chrome devTools performance 分析頁面如何渲染

2023-02-27 18:00:31

頁面是如何渲染的?通常會得到「解析 HTML、css 合成 Render Tree,就可以渲染了」的回答。但是具體都做了些什麼,卻很少有人細說,我們今天就從 Chrome 的效能工具開始,具體看看一個頁面是如何進行渲染的,以及進行頁面優化時需要關注哪些指標。

以「老二次元」網站 bilibili 為例,我們將通過分析 performance 面板,串聯起 Chrome 頁面渲染流程,以及頁面的部分量化指標的含義,來看頁面具體是如何渲染的。

獲取performance資料

首先,開啟Chrome devTools, 選擇 performace面板,點選錄制按鈕開始錄製。

之後為了防止我們分析頁面時出現無關的干擾,我們通過以下步驟降低干擾項:

1、開啟 Chrome 無痕模式。

2、關閉所有在 Chrome 無痕模式下啟用的拓展(如果有的話)。

3、在位址列輸入 www.bilibili.com 前,先開啟 devTools,選擇 performance 面板,點選錄制按鈕。

4、在已經錄製的情況下,位址列回車,請求 B 站,大概 10s 後,停止錄製。

我們從上到下,將圖分成以下幾塊,如下圖所示:

1、控制面板

2、概覽面板

3、網路面板

4、Web Vitals

5、執行緒面板

6、記憶體面板

7、聚合面板

控制面板

控制面板有 4 部分內容,分別為:

  • disable javascript samples:啟用後會隱藏一些 JS 呼叫棧的展示。在一些效能較弱的裝置例如行動端上,可以開啟這項功能。

  • Network:可以用來模擬各種網路狀況。

  • enableadvanced paint instrumention (slow):啟用後 paint 面板會顯示與繪製相關事件的更詳細的資訊。
    CPU:可以用來模擬不同的 CPU 效能。

概覽面板

概覽面板是各項指標的一個概覽,包含了 FPS 幀數、CPU 佔用、NET 情況、記憶體使用情況等。

簡單舉個例子,比如 FPS 幀數可以直觀的看出 FPS 的高低,綠色代表低的部分。而 CPU 欄的黃色代表著 js,紫色代表計算樣式和佈局,綠色代表繪製。

網路面板

網路面板用於展示正在請求中的各部分的組成情況。

Web vitals

Web vitals 是網站的 Web 體驗指標,其中包括 LCP(最大內容繪製)、FID(首次輸入延遲)、cls (累計佈局偏移)等。

執行緒面板

執行緒面板用於展示渲染當前頁面所使用到的執行緒,包含有 Main 執行緒、GPU 執行緒、Raster 執行緒、Chrome_ChildIOThread、Compositor 執行緒等等。其中 Main 執行緒,就是我們平時說的大部分 js 的執行環境,即主執行緒。

記憶體面板

展示 js 記憶體、GPU 記憶體、節點數、監聽事件數的變化。

聚合面板

當點選主執行緒中的火焰圖時,此面板會顯示顯示具體包含執行時間、執行組成、呼叫棧等等的資訊集合。

Chrome是如何渲染頁面的?

第一個請求

以第一個請求為例,我們來具體看一下 Chrome 是如何進行頁面渲染的?依然是以對 https://www.bilibili.com 的請求為例,來看一下 1ms 的 performance 面板,即下圖中紅線部分、中間 NET 欄藍色細長條開始的部分和 Network 中水平箱線圖開始的部分。

其中兩邊橫線中間深淺色方框的部分是水平箱線圖,是用來展示某部分在整體中的比例關係。比如我們看到這個長長的箱型圖,通過直觀感受,就能知道對前面一部分橫線挺長的,藍色部分裡淺色部分很長,深色的短,右邊的橫線幾乎看不到。那這些又分別能展示什麼資訊?

首先,點開箱型圖最下方的聚合面板(Summary),上面赫然寫著:此乃頁面源。欲求小破站, 終生皆讓我……耗時一秒半。

然後在 Network tab 裡檢視該請求的 timing 部分,可以得到如下圖:

這裡的各個部分分別代表:

  • Queueing(排隊):瀏覽器會在一些情況下讓請求排隊等待,比如這個請求的優先順序不高,有更高優先順序的請求存在;在使用 HTTP/1.0 或者 HTTP/1.1 時,同域請求最大並行數量為 6 個,此時已經達到了最大值;而上圖中的請求是屬於最高優先順序的第一個請求,即瀏覽器正在硬碟快取中分配空間,從圖上可以看到有 14.72ms 用於在磁碟快取中分配空間。

Stalled(停頓):它可能會因為上述排隊中的任何原因而停頓。

  • DNS lookup(DNS 查詢):解析這個域名的IP地址。需要注意的是,當我們多次存取同一域名時,這部分不會出現在 timing 中。

  • Initial connection(初始連線):瀏覽器建立連線,包括 tcp 三次握手、重試以及協商 SSL。圖中的紫色部分,就代表了在初始連線過程中的 SSL 協商部分。

  • Request sent(傳送請求):正在傳送請求

  • Waiting (TTFB) 等待第一位元組時間:瀏覽器在等待第一個響應的位元組,TTFB 即 Time To First Byte。這個時間包括一個往返的延遲和服務準備響應的時間之和。

  • Content Download (內容下載):瀏覽器正在接收響應,瀏覽器可以通過網路或者 serviceWorker 來直接接收。這個值是讀取響應體的總時間。由於網路不佳或者瀏覽器正在忙於執行其他工作而延遲了對響應體的讀取,讀取的時間可能會比預期的要長。

這裡相信已經有小夥伴注意到了,當瀏覽器忙於其他事情時也會讓讀取時間變長。也就是說,當你的 js 把主執行緒長期佔據的時候,就會影響 content download。

下圖是 Network 下的對應資源的 waterfall:

現在我們回到最開始說的各色橫條上,在水平箱線圖中左上角的深藍色小方塊代表著這個請求有著更高的優先順序。遇到有淺藍色的,則表示較低優先順序。同時左邊橫線對應 Network 面板中顯示的 Request Sent之前的所有事情的時間。淺色的 bar對應 Network 中 Request Sent 和 Waiting(TTFB)的時間。深色的 bar對應 Network中Content Download 的時間。右邊的橫線表示等待主執行緒所花費的時間,在 Network 面板中沒有體現。

此外,可能還有些同學注意到,在藍色箱線圖上面還可以看到還有幾個灰色的箱線圖。不是說www.bilibili.com 是頁面的第一個請求嗎,難道它之前還有請求?

事實上,這個灰色箱線圖相當於上一個頁面的結束。如果我們是通過重新錄製的方式記錄 performance,那就會經歷頁面重新整理的過程。而這幾個灰色的其實就是頁面重新整理 unload 時發起的,是 bilibili 用來記錄頁面解除安裝時的一些資料。

說回到箱線圖,可以看到在 summary 中顯示 Duration 1.08 s (822.88 ms Network transfer + 260.20 ms resource loading)。這個的意思是 260ms 的時間是在 resource loading ,這裡resource loading所花費的時間其實就是箱線圖右側的那條橫線,等待主執行緒的時間。

而在 main 程序中,有橫線結束的地方,可以看到解碼的資料 138,933 Bytes。

這裡就出現了幾個問題:為什麼Encode Data 33479 bytes 算下來是 33479/1024 = 32.69 k,而不是前面 Network 面板裡的 33.5k ? 而且 Decode body 138993/1024 = 135.7k 也不是前面的 139k?缺少的一部分資料是什麼呢?

為了驗證這個問題,需要清空過去所有請求記錄,重新點選錄制,錄製完成後,匯出網路請求的 HAR 檔案。使用 vscdoe 開啟 json 格式的 HAR 檔案,尋找 GET https://www.bilibili.com/ content-Type: text/HTML 的那個請求。經過前後的檔案對比,找到了這個請求的 response content:

可以看到,圖上的 size 有140682 位元組。text則是 base64 編碼的 HTML 內容,已經被 decode 過。需要注意的是,這裡的 decode 不是對 base64 的 decode,是對 gzip 的 decode。

而在這個 text 內容之後,還有一段如下內容:

其中的 _transferSize: 35593 是網路傳輸的體積,即傳輸的體積 35593 和 decode 體積 140682。同時我們在 performance 裡的主程序中的 finish loading中可以看到下圖資料:

這樣一看,二者是相同的。說明這個 HTML 的傳輸體積就是 35593 Bytes。

那為什麼在 Network 面板裡,我們看到的是 35.6k transferred over Network 呢?

這是因為在 Network 裡展示的體積,不是除以 1024 計算的,而是除以 1000,然後四捨五入後的結果。

不過 Summary 裡的 pending for xxx ms,似乎是也是等待主執行緒的時間,但它又是如何在 performance 體現的。目前,我還沒搞清楚,如果有了解的小夥伴歡迎留言討論~

請求其它資源

言歸正傳,我們現在獲取到了 bilibili 網站的 HTML,接下來就需要對這個 HTML 進行處理。

通過 response header 得到 content-type:html,此時會建立一個個渲染程序,也就是主執行緒的這個程序。但是可以看到在主執行緒中的藍色 parse HTML 之前,已經有很多 set request 被髮起了,而且這些 send request 都是 HTML 檔案中的一些 js 和 css。

為什麼會這樣呢?不應該是先解析 HTML,才能知道對哪些資源進行發起請求嗎?

在 HTML 中引入的 js,存在修改 Dom 的可能,所以瀏覽器一般在遇到 script 標籤後,會先暫停 HTML 解析,優先 js 的下載和執行。但是下載是相對耗時的,如果因為下載時間久而卡住了頁面解析,很容易導致使用者體驗變差,因此 Chrome 採用了一些優化策略。

具體來說,就是當 Chrome 渲染引擎接收到 HTML 的位元組流時候,會開啟一個專門用來分析位元組流中所包含 js、css 檔案的預解析執行緒。解析到相關資訊之後,預解析執行緒會提前開始下載這些資原始檔,這樣在需要使用的時候就可以直接執行,避免了下載的等待時間。

但是也能觀察到,在Parse HTML藍色方塊下方,還有一些 send request,這些怎麼就不是提前下載的呢?
我的理解是,這些資源其實都是在預解析執行緒下載的,儘管在時間上會存在重疊,但和主執行緒不屬於同一個執行緒,所以 performance 工具會這麼顯示。但這又帶來了另一個問題,為什麼有些 js 明明在 HTML 的後面,卻在前面就 send request 了,而有些 link/script 明明寫在 HTML 裡的前面,卻在 performance 裡後 send request?

這是跟資源的優先順序有關。比如普通的 script 標籤參照的資源,普通 link 參照的資源,或是rel=prelaod 或 as="style"預載入的資源,可能會被優先處理。而當資源是 prefetch,或者用 這種方式的,由於優先順序低,就會被延後下載。一般的其他資源,則按順序下載。

回到 Network,可以看到在 www.bilibil.com 的箱線圖之後,是一連串 js、css、Webp 資源需要載入的請求被髮起了。把滑鼠移動到這些箱線圖上,會看到上面有優先順序 lowest low high highest,這就表示了資源的重要程度。

那麼這些資源的優先順序是如何評定的?一般來說,存取域名獲取的 HTML、 以及預載入資源時as="style",擁有最高優先順序。普通的