本文由體驗技術團隊董福俊原創。
一、背景
在做前端頁面性能優化時,Performance面板是一個必不可少的工具。這個工具比較強大,既可以從全局視角分析整個網頁加載情況,又可以從代碼細節,挖掘某個具體環節的性能情況。但這個工具的結果可能讓人看着感覺難以理解,因為它本質上是將各種運行信息呈現給我們,而不是直接告訴我們問題在哪兒。我們需要將各類信息:瀏覽器加載過程、框架打包(webpack)、代碼編譯(tsc、babel)、代碼執行(angular)等,綜合起來並跟Performance面板上的環節一一對應上,然後才能分析出問題。
本文首先簡單介紹Performance面板上的一些高頻使用的工具/按鈕,然後重點介紹Network面板跟Main面板的對應關係。
二、Performance面板基本介紹
Performance面板一般用於錄製某個操作過程中,瀏覽器的"內心活動"。包括網絡情況、js執行情況、幀渲染情況等。
一般錄製步驟是:啓動performance記錄,到網頁上做一段操作,停止錄製。然後面板上會展示結果,大概長這樣:
下面,一一介紹一下圖中這些工具的作用。
1. 操作按鈕
- 啓動/停止:點一下啓動performance錄製,然後到頁面上做特定操作,再點一下結束。
- 啓動並重載頁面:如果要研究頁面加載過程的性能,可以直接點這個。(點它 = 點啓動 + 刷新頁面 + 點停止)
- 清空:清空當前分析結果
- 保存分析結果:將當前結果保存到本地,以後再用
- 加載分析結果:從本地加載某個曾經保存的分析結果,接着分析
- 是否保存截屏:這個勾選框需要在啓動錄製前就決定是否勾選。如果勾上了,那麼錄製的時候,會把每一幀的可視區截屏記錄下來
- 是否顯示內存:是否顯示內存面板,它裏面記錄着網頁內存使用情況的概要
- js垃圾回收:一般啓動錄製之前,多點幾下
這些按鈕沒有特殊之處,多操作幾次就能熟練。
2. Network面板
Network面板是會被高頻使用的面板,它體現了網絡下載情況。常常跟後面的 Main面板、Thread pool面板 聯合起來分析具體情況。面板中每個色塊都代表一個請求,顏色表示請求類型,一般情況下:
顏色如果記不住,直接將鼠標挪到色塊上的彈窗中,也會體現這個請求的類型。
一般的,一個請求有以下四個階段:
- Queuing and connecting:Queuing指的是瀏覽器還沒有正式處理這個請求,需要排隊等待;connection指瀏覽器跟服務器在建立TCP連接。
- Request sent and waiting:表示請求已發送,請等待。也就是HTTP request已經發送到服務端了,等着服務端響應。
- Content downloading:表示瀏覽器已經收到了服務器響應(收到了response head),正在下載response content。
- Waiting on main thread:表示response content已經下載完了。但是主線程太繁忙,沒有時間執行這個請求的回調函數。
詳細介紹見下文 1. 網絡請求中的4個環節
3. Frames面板
體現渲染幀率情況,反應頁面流暢度。一個格子對應一幀,也就是表示瀏覽器嘗試了一次去繪製屏幕。格子常見有3種顏色:
- 綠色塊:表示幀被完整渲染並呈現。
- 黃色塊:表示幀沒有完成全部渲染,只呈現了部分內容。
- 紅色塊:表示幀沒有完成渲染,被瀏覽器丟棄。
- 白色塊:空閒幀,表示當前沒有需要執行的渲染任務了。
注意:格子顏色與幀的時間長短無關。
4. Animations面板
體現合成器線程的情況,它主要反應了我們使用的css animations或transition的執行情況。 每個色塊對應着一次正在運行的animations或transition,並且點擊這個色塊,能在摘要面板(summary)中看到着一次執行的相關信息(特別是被執行的DOM節點會體現出來)
5. Timings面板
記錄一些時間信息,包括頁面加載的關鍵時間節點。並能體現出如console.time、performance.mark等。 (PS:在代碼中寫的performance.mark會體現為這裏的一條條細線,而console.time會體現為這裏的一個個色塊。我們在代碼的關鍵節點增加一些mark、time,就能在分析面板中直觀的瞭解當前的頁面加載進度)
6. Layout shifts面板
記錄佈局偏移情況。佈局偏移是指頁面上一個可見元素的位置發生了移動,就會被記錄在這裏。每次偏移都對應截圖中的一個小點。
可見元素的位置發生移動,會引起視覺不適,所以一般要儘量避免佈局偏移。而常見會引發佈局偏移的動作有:動態插入一些內容,一些DOM操作等。
7. Main面板
記錄瀏覽器主線程的活動,我們常説js是單線程的,指的就是js運行在這個主線程上。每個色塊對應一個任務(注意:不是宏任務/微任務的那個任務)。
色塊從上到下是包含關係,指的是:上一層的色塊(任務)是由下一層的若干個子色塊(子任務)組合而成的。
色塊從左到右是並列關係,指的是:並列的若干個子色塊,是從左到右執行的。(左邊色塊如果有子色塊,那麼會先把子色塊執行完成後,再執行右邊色塊)。
詳細介紹見下文 3. 主線程對請求結果的處理
8. Thread pool面板
記錄worker線程的執行情況。worker線程主要用於執行耗時、計算密集型且可以並行化的後台任務。
Thread pool是一個線程池,這個池子中開出來的線程數,是由瀏覽器根據任務類型、系統資源、硬件能力等因素動態調整的結果。一般情況下,開出來的線程數會接近或略多於CPU的核心數。網絡請求的結果,會放在某個線程中進行解析和編譯。
詳細介紹見下文 2. worker線程對請求結果的解析編譯
三、Network面板、Main面板及Thread pool面板
一個靜態資源加載過程的各個階段中,網絡傳輸階段體現在Network面板,內容解析編譯階段體現在worker線程,資源的處理階段體現在主線程。聯合分析三個面板就能掌握詳細的靜態資源加載全流程。
1. 網絡請求中的4個環節
前面對Network面板的介紹中提到,一個HTTP請求會經過4個階段:Queuing and connecting、Request sent and waiting、Content downloading、Waiting on main thread。這4個階段從HTTP報文的視角,會比較好理解。
1.1 “一問一答”的HTTP請求
眾所周知,HTTP請求是一問一答的模式(下圖右半部分),HTTP報文是字符串格式(下圖左半部分)。
我們在代碼中寫的 <script>、ajax、fetch,都會讓瀏覽器去發起一個HTTP請求,這個請求就會經過以下4個階段:
- Queuing and connecting
瀏覽器知道了要發請求,但它正忙着建立Tcp鏈接或別的事情,所以這個請求要先等一等。
- Request sent and waiting
瀏覽器終於開始處理這個請求了。它按HTTP報文格式,將Request Head發到Server,然後等Server的回覆。此刻Server正忙着解析這個請求,思考該如何回答這個問題(例如從服務器磁盤上讀取某個文件來響應這個請求)。這個過程中瀏覽器一直在等待,雙方在TCP層會通過Keep-Alive報文(如果有必要的話)來保持鏈接。 (PS:當服務器準備好響應內容後,它會按HTTP報文格式的規定,先後返回Response Head和Response Body給Browser)
- Content downloading
表示當前瀏覽器已經收到Server返回的Response Head,它正忙着接收Response Body。瀏覽器會在worker線程中解析編譯Response Body,不佔用主線程。只有當整個Response Body完整的下載解析完成後,才將解析好的結果移交給主線程去執行。
- Waiting on main thread
瀏覽器主線程收到了worker線程移交過來的解析結果,但主線程太忙,沒有時間處理。(也就是沒有時間觸發回調函數)
1.2 HTTP/1.1及HTTP/2
頁面初始化時往往會有大量請求要發出去,這就導致請求往往會卡在第1個階段 Queuing and connecting。
- HTTP/1.1
在HTTP/1.1協議中,請求是一問一答的模式,所以前一個請求沒有被回答時,後一個請求就不能發出去(否則收到響應後會搞不清楚回答的是哪個請求)。瀏覽器為了提升請求速度,往往會跟Server同時建立多個TCP鏈接。
HTTP協議是建立在TCP鏈接之上的,而TCP鏈接是靠四元組(客户端IP+客户端端口號+服務端IP+服務端端口號)來區分的。在我們討論的場景中,瀏覽器是客户端,Server是服務端,一般客户端和服務端的IP是唯一的,而服務端可能要同時為多個客户端服務,所以服務端端口號比較珍貴。那麼,瀏覽器為了跟Server同時建立多條TCP鏈接,就只能選擇在客户端上開多個端口號來建立多條TCP鏈接。
所以,瀏覽器一般對同一個域名會建立一定數量 (見 説明1 **) **的TCP鏈接(瀏覽器開多個TCP端口,服務端只開1個TCP端口)。但這也只是緩解請求排隊現象,如果請求數超過TCP鏈接數,那麼靠後的請求還是得依次排隊等待可用的TCP鏈接。
有些場景下,為了進一步提升請求速度,我們也可以採用域名分片方式來提升下載速度,即:將靜態資源放到多個域名下,這樣瀏覽器就能開更多的TCP鏈接去下載資源。但很顯然,HTTP/1.1協議慢就慢在它必須按順序一問一答的去處理HTTP請求,前一個請求沒有響應完,後一個請求就不能發出去,這個問題叫 隊頭阻塞。這就好像你去超市買1瓶礦泉水,到收銀台買單時發現隊伍排的很長,隊頭第一個人有一滿購物車的東西等着掃碼。此時即使再多開幾個收銀台,也只能緩解排隊。為了根治這個問題,HTTP/2中引入了多路複用機制。
- HTTP/2
為了根治隊頭阻塞問題,HTTP/2協議引入了多路複用機制,它允許同一個TCP鏈接上可以同時發多個HTTP請求。也就是説瀏覽器可以一次性把多個HTTP請求的request head都發出去,並允許大家的響應交叉返回。所以這種情況下,瀏覽器一般只需要建立1個TCP鏈接即可。
從以上分析可以看出這幾點:
- HTTP/1.1協議下,同一個域名下的靜態資源,才會互相競爭網絡。
- HTTP/2協議下,同一個域名下,多個HTTP請求能併發。
2. worker線程對請求結果的解析編譯
通過網絡請求,瀏覽器拿到的是字符串形態的代碼,它們需要被解析、編譯之後才能執行。前面介紹Thread pool面板時提到過,worker線程一般用於執行耗時、計算密集型且可以並行化的後台任務。而網絡請求結果的解析和編譯,正好適合放在worker中進行。
例如,我們在代碼中寫了一個<script>標籤,瀏覽器會通過HTTP請求去下載這個js文件。請求的Response Body中就是js文件的內容,但內容是字符串形態的js代碼,它需要先解析編譯才能執行。
- 解析:對字符串形態的js代碼進行語法分析,產生AST(抽象語法樹)
- 編譯:V8引擎的編譯器根據AST,生成字節碼或機器碼
- 執行:V8引擎逐條運行這些字節碼或機器碼
注意:瀏覽器會在 worker線程中完成解析&編譯環節,然後將結果移交給主線程去執行。 (PS:在Thread pool面板的worker中看到大量的Parse and compile塊,就是解析和編譯)
(PS:通過點擊Thread pool worker中的色塊,Summary面板會聯動展示相關信息,可以看出這個色塊是在解析哪個靜態資源)
由於Thread pool線程池裏面有多個線程(worker),所以每個靜態資源很容易分配到一個worker進行解析。而且現代瀏覽器一般是流式解析和編譯,也就是上圖中的Streaming compile task,它表現為:每收到一個數據塊就解析一個數據塊。
一般的,解析和編譯往往不是速度瓶頸,網絡下載才是。現代電腦硬件和瀏覽器能力,往往能迅速完成對數據塊的解析和編譯,大多數時間都消耗在等待其餘的數據塊。
3. 主線程對請求結果的處理
當worker線程完成對解析、編譯之後,其產生的字節碼或機器碼將會被移交到主線程中去執行(如果是webworker加載的js,則會被移交到對應的webworker線程中),我們看到的Main面板就是反應主線程的活動情況,這個圖一般叫火焰圖。
面板中task下面的Evaluate script和Evaluate module(聲明瞭type="module"的script)色塊就是在執行代碼。一般在Evaludate色塊下面的(anonymouse)色塊表示的是頂層代碼,可以理解為一個超大的匿名函數,將所有待執行代碼包裹其中。
另外,在圖中我們還看到Evaluate script下面出現了Compile script,這是因為V8引擎的惰性編譯策略。worker中的編譯動作並不會將整個代碼中的所有函數一次性全部編譯掉,它通常只編譯頂層代碼和可能會立即要被調用的函數,這樣能避免編譯非必要代碼。所以在Evaluate script的過程中,如果主線程要執行一個尚未被編譯的函數,那麼它會通過即時編譯器(JIT) 來編譯這個函數。另外,有一些代碼(例如內聯script)只能在主線程中編譯。同樣的,Evaluate module下面會出現Compile code,可能是因為它要執行一個從別的module引用過來的函數,需要先JIT編譯。
色塊代表着任務,任務的種類很多,但大多數的色塊表示的就是一個函數,色塊的名字就是函數的名字。所以色塊的上下順序就表示函數見的調用。但是色塊上很容易出現一個一些奇怪的名字,例如:
- anonymouse:匿名函數,代碼中的各種回調函數,往往都是匿名函數(也有使用具名函數當回調的,那麼色塊就會顯示函數名)。
- 一些英文字母:例如 c.O、L、U等。當我們針對生產環境抓包時往往會遇到這些。這是因為代碼打包時往往會做代碼混淆,將原始代碼中的變量名、函數名換成無意義的短名稱。
- 一些數字:webpack打包時,會將多個js文件打成同一個chunk。而在chunk中,每個文件都會被包裹在一個匿名函數中,並放在一個對象中同時生成一個數字作為key。
一般的,奇怪的色塊名字,基本都是打包工程處理的結果。如果想要看到這些函數的真實名字,則還需要找到打包產生的SourceMap文件,導入到瀏覽器中。一般情況下,我們可以同時對本地代理和生產環境錄製performance,兩相對比之下,也大致能搞清楚各個色塊大概是在做什麼。
如果我們針對本地代理localhost錄製performance,則往往能看到真實的函數名。但仔細對比,有時會發現,有一些被調用的函數並沒有出現在色塊裏面。比如:fn1調用了fn2、fn3、fn4,但色塊fn1下只有fn2和fn3兩個色塊,fn4不見了!這可能是因為火焰圖是採樣圖,Chrome默認1000Hz(每1ms採集一次調用棧),所以如果一個函數的執行時長 <0.1ms,那麼這個函數可能被漏採樣了,所以就沒有它對應的色塊。當然,也有可能單純就是這個函數沒有被執行。
四、總結
Performance面板將瀏覽器的“內心活動”都錄製下來展現給我們,但是從錄製結果中找到問題原因的過程,是複雜且需要大量綜合信息支撐的。其中,對每個面板所呈現的信息進行理解是最基本的前提,只有在此基礎之上,我們才能結合我們的工程配置、代碼情況 以及各種前端相關技術知識,來對我們的頁面有個綜合診斷,再然後才是擬定優化方案並實施階段。如果前面的基礎都沒有找對方向,那麼最後的優化結果也只能是各種碰運氣。在實戰中,錄製結果也往往是動態的,即使是同一個頁面,在不同時間、不同CPU負荷、不同網絡情況下的結果都可能會有不同,所以需要多次錄製並在不同結果中找到相同的問題點,進行重點優化。
在下一篇文章《聊聊Performance面板2——網頁加載實戰演練》中,我們將結合一個實戰案例來説明具體的分析過程。
五、附錄
説明1. HTTP/1.1下瀏覽器對同一個域名同時建立的TCP連接數
坊間傳聞,一般chrome默認是最大6個TCP連接。
但真實情況中,有見過10個TCP連接的,如下圖所示:(在DevTools的網絡面板中,將connection ID列放出來,每個connection ID對應一個TCP連接)
對於這種顯現給,也有另一種説法認為,是在開啓devtools之後,瀏覽器會臨時突破連接限制(觀察者效應)導致的,但具體原因尚不明確。
關於 OpenTiny
歡迎加入 OpenTiny 開源社區。添加微信小助手:opentiny-official 一起參與交流前端技術~
OpenTiny 官網:https://opentiny.design\
OpenTiny 代碼倉庫:https://github.com/opentiny\
TinyVue 源碼:https://github.com/opentiny/tiny-vue\
TinyEngine 源碼: https://github.com/opentiny/tiny-engine\
歡迎進入代碼倉庫 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor\~
如果你也想要共建,可以進入代碼倉庫,找到 good first issue 標籤,一起參與開源貢獻\~