一、背景
關於Performance面板的基礎用法介紹,可參考上一篇文章《“Performance面板”一文通,解鎖前端性能優化工具基礎用法!》。文章中還從一個HTTP請求的四階段的角度來介紹Performance圖的"觀看方式",並重點介紹了worker線程跟主線程的協作關係
本篇文章中,我們將會以一個實際網頁 ——VPC列表頁為例,介紹Performance抓圖及分析的過程,並將上一篇文章中介紹的相關內容串起來,希望每位frontend developer都能掌握用Performance分析頁面性能的能力。
PS:這裏假設我們的分析目標是:讓列表頁的主內容區域儘快展示出來,以避免長時間白屏;另外,為求簡潔,下文敍述中統一將Performance錄製的結果圖叫做"性能圖"
二、分析前準備
正式分析之前,建議做一些準備工作,能有效提高後續的分析效率。
1. 環境準備
1)選一個良辰吉日
建議關閉一些應用程序、瀏覽器頁籤,儘量讓電腦CPU空閒一些,(也建議關掉通信軟件,讓自己心情好一些)。儘量避免一邊開會一邊分析,避免分析過程被頻繁打斷,因為分析過程可能遇到各種奇怪的現象,倘若再被其它事務打斷,心態直接崩潰,那麼就分析不下去了……
2)選瀏覽器無痕模式
建議在瀏覽器無痕模式下錄製性能圖並進行分析,因為一些特殊的瀏覽器配置、瀏覽器插件 都可能影響網頁的加載過程,干擾分析結果。
3)選生產環境進行分析
我們可能有多種不同環境:本地代理環境(俗稱localhost)、類生產環境、生產環境 等。只有生產環境是最真實貼近用户的,所以建議在生產環境下進行網頁性能分析。
但需要注意的是,生產環境部署的代碼,跟源碼往往有較大形態上的差異(eg:打包工程的混淆、部署時的加工、服務端渲染的處理等)。所以,生產環境的性能圖在某些代碼細節上可能無法深入研究,此刻,我們還是需要其它環境的性能圖進行輔助對比。
2. 代碼準備
1)瞭解代碼結構和加載流程
建議先從源碼角度,詳細瞭解待分析頁面的結構及加載流程,例如我們討論的樣例—— VPC列表頁的結構如下:
頁面結構
- 紅色框:由基礎框架console-ui提供的header、sidebar 實現
- 藍色框:由業務代碼實現,採用angular技術框架,NG App下掛載一個根組件(VpcComponent)
- 綠色框:根組件下分為 導航組件(LeftmenuComponent)和 列表頁組件(VpcListComponent)
代碼結構
<!-- index.html -->
<div id="header"><!-- console-ui負責渲染 --></div>
<div id="sidebar"><!-- console-ui負責渲染 --></div>
<!-- NG App 掛載點 -->
<div id="ngApp">
<!-- 根組件 -->
<vpc-component>
<leftmenu-component><!-- 導航組件 --></leftmenu-component>
<router-outlet>
<!-- 內容區-路由渲染點 -->
<vpc-list-component><!-- 列表頁組件 --></vpc-list-component>
</router-outlet>
</vpc-component>
</div>
- console-ui和NG App並行工作,分別負責不同div的渲染
- APP的根組件下,導航組件和內容區並行工作,內容區由路由加載列表組件
初始化流程
- 主渲染流程是:NG App啓動 => 加載根組件 => 路由渲染 => 加載列表頁組件
- angular中每個組件都會經歷生命週期:constructor => ... => ngOnInit => ... => ngAfterViewInit => ... (這裏只列舉一些常用生命週期鈎子,完整的説明見 組件生命週期。PS:這一點不理解也不妨礙下文閲讀)
2)關鍵節點加performance.mark
很關鍵的是:在我們的主渲染流程上一些關鍵的時間節點加上performance.mark,或者關鍵時間段加上console.time/timeEnd。這樣在性能圖的Timings面板上就會顯示這些標記,從而幫助我們確認各個執行環節。
例如,針對VPC列表頁的主渲染流程,我們在 NG App啓動、根組件constructor及ngAfterViewInit、列表頁組件constructor及ngAfterViewInit,分別加上performance.mark,則能在Timings面板中看到下圖情況:
PS1:Timings面板中的線很細,不仔細觀察可能會漏掉。
PS2:為了針對生產環境進行分析,我們可能需要提前在代碼中預埋performance.mark,待上線後才能利用的上。
3)錄製性能圖
在Network面板清空已有記錄,在Performance面板多點幾下垃圾回收,然後開始錄製。錄製完成後,將Performance面板的性能圖導出成json文件,並將Network面板的網路請求記錄導出成har文件,保存在本地以方便後續查看。
三、數據分析
錄製好性能圖後,建議先分析Network面板中的index.html,大致瞭解加載了哪些靜態資源,然後再投入性能圖的分析
1. 分析index.html
首先分析下Network面板中的index.html,大致瞭解頁面加載了哪些靜態資源。 一般源碼中的index.html跟瀏覽器實際執行的index.html往往有很大差別,因為代碼(打包)工程會對index.html進行魔改,如果有服務端渲染機制,那麼服務渲染時可能還會插入一些樣式、腳本。所以,瀏覽器最終執行的index.html將會是 源碼+工程修改+服務端渲染 之後的結果。
例如,針對VPC列表頁的html進行分析,會發現其加載了以下資源(按html中從上到下的順序排列)
我們需要搞清楚瀏覽器會開幾個TCP連接?哪些資源會擠在同一個連接中?因為同一個連接中的資源會互相爭搶網絡帶寬 。
分辨方法是:h2下同一個域名的資源會共用同一個TCP連接,http/1.1下同一個域名可能會開多個TCP連接,資源們按順序排隊。 所以上表中,所有CDN域下的資源共用同一個TCP連接,Server域下只有一個資源,暫時只開一個TCP連接。
PS:説“暫時”是因為,後續可能會有動態加載js、或者發起ajax、fetch請求,可能會增加TCP連接數量。
另外,下載優先級是由瀏覽器綜合分析並自動分配的,我們無法直接指定優先級。並且不同版本瀏覽器的分配策略各異,但大多數情況下,會遵循如下規則:
- 通過
<script>標籤加載的js腳本的優先級高於動態創建script的優先級。(動態創建例如:通過appendChild往DOM中插入一個<script>標籤) <script>標籤上沒有加任何標記(module、async、defer)的,優先級最高<script>標籤被標記了type="module”的,優先級較高<script>標籤被標記了async、defer的,優先級較低
值得説明的是,項目採用了webpack打包,其中main.{hash}.js就是webpack主入口文件,而vendor\~tinycloud等文件均是分包策略拆出來的chunks們。
2. 分析Performance數據
接着,我們就要分析性能圖了,我們的首要目標是:搞清楚性能圖中各個環節在做什麼,並將它跟初始化流程一一對應起來。
2.1 分析映射關係:代碼 <=> 性能圖
首先觀察性能圖中 Network(網絡情況)、Main(CPU情況)、Timings(我們預埋的mark標記) 這三個部分,嘗試搞清楚圖中每個時間段裏面,瀏覽器在忙些什麼。 以上圖為例,大致流程是:
- 時段1:網絡繁忙、CPU空閒
- 時段2:網絡空閒、CPU繁忙
- 時段3:網絡CPU都繁忙
- NG App啓動從時段3才開始
接下就是詳細分析過程:
1)時段1:靜態資源下載
分析每一個請求,搞清楚它是誰發起的,在業務上有什麼作用。 點開一個資源條,就可以在Summary面板中看到它的一些基礎信息。
PS:有時候,Summary面板中寫的Initiated by不一定是真實的發起者,真實發起者可能被層層代碼封裝隱藏了,我們需要到Network面板中的Initiator裏面去找真實發起者
對時段1的所有請求進行分析之後,我們就搞清楚了這個階段的具體情況,如下:
-
綠色部分由console-ui發起
- 首輪請求(html中通過script直接引用):僅consoleui.umd.js這1個資源 (CDN域/h2)
- 非首輪請求(由某些邏輯動態發起):5個靜態資源 (CDN域/h2) ;若干Fetch/XHR請求 (Server域/http1.1)
-
紅色部分由業務代碼發起
- 首輪請求(html中通過script直接引用):runtime、polyfill、……、main等11個資源 (CDN域/h2)
- 非首輪請求(由某些邏輯動態發起):無
-
藍色部分由cc組件(一個三方業務組件,負責一些特殊業務組件的實現)發起
- 首輪請求(html中通過script直接引用):無
- 非首輪請求(由某些邏輯動態發起):cc-main.js (Server域/http1.1) 及若干main、theme等 (CDN域/h2)
時段小結: 這一時段主要是完成各種HTTP請求,主要有 業務代碼、console-ui、cc組件三方參與。首輪請求主要是業務代碼的請求,採用h2協議,console-ui及cc組件則多為非首輪請求。
這裏其實可以看出來,業務代碼發起的靜態資源請求幾乎獨佔首輪請求,其它請求均是在後續輪發起,不會影響業務靜態資源請求的完成。並且所有Fetch/XHR請求都是使用 Server域/http1.1,跟靜態資源使用的 CDN域/h2是兩個不同的TCP通道,不會影響業務靜態資源的加載。
2)時段2:webpack代碼展開
分析火焰圖中每個色塊,搞清楚它們是屬於哪個代碼文件的內容,它們在執行什麼?
上一篇文章中介紹過,每個色塊是一個函數,色塊的名字是函數名,色塊上下關係是函數調用關係,這一整個火焰圖就是調用棧的直觀展示。
- 查看色塊歸屬:在上一篇文章中提到過,webpack打包時會將js代碼用匿名函數包裹,並指定一個數字key,所以就產生了這些數字命名的函數。點擊色塊可以看到它屬於哪個代碼文件。
- 查看色塊在做什麼:查看這個task的火焰圖的棧底,會發現全是黃色的Compile code塊,説明在編譯代碼(V8引擎的惰性編譯策略)
- 整個展開動作的起點:等待初始chunk全都下載完之後,才開始代碼展開。
時段小結:這一時段主要在做webpack的代碼展開,CPU在忙着編譯、執行被webpack打包的代碼。
這裏需要對webpack打包產物有一定了解,才能透徹瞭解這個過程,簡要説明如下:
標題中所謂的webpack代碼展開,其實就是執行這些數字命名的包裹函數,而V8引擎的惰性編譯策略,可能不會在流式下載文件的環節就直接編譯這些代碼,所以棧底全都是compile code的色塊。另外,webpack的啓動機制會保證所有chunk下載完成後,才啓動代碼展開工作,從 時段1 => 時段2 的銜接點可以看到,雖然主chunk文件main.xxxx.js早已下載完成,但代碼沒有立即展開,而是等待最後一個chunk下載完成之後,才進行展開。
3)時段3:動態下載語言包等主題資源
利用時段1提到的分析方法,對時段3的請求也做同樣的分析可知:
- 主要是語言包、主題資源包、docs等通用資源的下載,主要使用 CDN域/h2 的通道。
- 均是由業務代碼發起。
利用時段2提到的分析方法,對時段3的火焰圖的各個色塊也進行同樣的分析可知:
- 一些由微任務或定時器,喚起的console-ui邏輯被執行
- 一些cc組件邏輯被執行
- 還存在着若干空閒task時間
同時,從Timings面板可以看出,在webpack代碼展開之後、時段3開始之前,NG App就已經boot了,但是在時段3結束後,根組件才開始construct。
為什麼angular的app已經開始boot了,但根組件沒有儘快construct?並且這中間還有空閒的CPU時間。 這裏需要結合代碼實現來分析:
- 該頁面支持多語言,為了不一窩蜂的下載所有語種的資源包,代碼中採用按需下載的模式:僅在確認了當前語種之後,才開始下載該語種的資源包。
- 代碼中利用angular router的路由守衞,在守衞中異步下載當前語種對應的資源包。angular router會確保根組件在守衞完成後再開始construct。
所以,問題的答案是:根組件在等待資源包的下載完成。從圖中也可以看出,根組件construct是在docs接口完成之後的第1個task中進行的。
時段小結: 這一時段主要是下載當前語種對應的資源包,而這些資源包是根組件construct的前提條件(因為路由守衞的原因) 。
而在資源下載期間,CPU被用於執行一些console-ui、cc組件等非業務邏輯,或者直接空閒。因為在這期間,也沒有業務邏輯代碼可供執行了。
4)時段4:關於時段3之後
從Timings面板中可知,在時段3之後、特別是根組件construct之後,根組件afterViewInit、列表組件的construct和afterViewInit都緊鑼密鼓的執行起來了。這一段CPU極其繁忙,按生命週期順序執行各個組件的初始化,直到列表組件的afterViewInit完成後,列表頁主內容區域才展示出來。
針對這種任務密集時段,我們當然也可以用時段2中提到的分析方法,針對火焰圖中各個色塊進行分析。但從生產環境錄製的性能圖中看到的色塊名(函數名)可能都是混淆之後的結果,不利於跟源碼對應起來。此時,我們可以針對本地調試環境(localhost)錄製性能圖並進行分析,因為代碼執行在火焰圖上的表現,往往不會受到環境的影響。例如該時段中有這麼一段:
可以看出有一段結構很相似的調用,出現了多次重複。從localhost環境抓取的性能圖中就可以明顯看出,這段重複執行的是 detectChangesInEmbeddedViews 函數,它是angular的內部函數。在嵌入式視圖場景中,如果有大量ngFor、ngIf就會觸發。通過源碼分析,這一段正是導航組件中的代碼實現造成的。
2.2 在性能圖中找問題
通過上一節中對性能圖透徹分析之後,我們已經能將性能圖跟源碼對應起來,並且能將性能圖跟加載流程對應起來,同時對一些關鍵節點心中有數。接下來要做的就是找問題、找可優化的空間,以達到我們的目標。例如針對VPC列表頁的分析可知,主渲染流程在性能圖中大致是:
為了實現目標,我們至少可以找到以下優化點:
-
下載靜態資源能否更快?
- 非重要模塊,改為懶加載(按需動態import),減少初始chunk的體積
- 較大的圖片資源等,避免打包成base64字符串,減少chunk體積
- 合理拆分chunk包,避免有一個獨大的chunk,充分利用h2的並行下載效果
-
webpack代碼展開能否更快?
- 減少初始chunk的體積。需要展開的代碼少了,展開的自然更快
- 避免在代碼中執行長耗時運算等
-
下載語言包資源能否更快?
- 語言包資源提前下載,和前面的靜態資源一起下載。
-
組件生命週期能否執行的更快?
- 削減高頻重複執行的detectChangesInEmbeddedViews函數的執行次數。
- 減少非必要的渲染內容。
總體上看,不同的項目會有不同的性能圖和性能瓶頸點,通用的優化方案可以解決一些常規問題,但當所有常規方案做完之後效果還是不夠滿意時,我們可能得進行針對性的分析來查找問題。通過深入性能圖分析,搞清楚 代碼<=>性能圖 之間的映射關係之後,我們就能輕鬆找到性能瓶頸點,從而找對應的解決方案了。
四、總結
通過Performance面板錄製頁面加載性能圖並進行性能分析,是每一個frontend developer進階的必備技能之一。性能圖分析除了要求我們掌握Performance面板的基本用法之外,還要求我們對前端相關知識例如:webpack工程打包、瀏覽器的加載運行、HTTP協議機制、前端框架的原理、等都有一定了解,同時要求我們對項目代碼的結構和執行流程足夠清晰明確。常規的優化方案往往只能解決一些初級、普遍的問題,但每個頁面有每個頁面的具體情況,只有對頁面進行充分分析之後,才能搞清楚頁面的性能優化點在哪裏,從而有條不紊的落地實施。
關於 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 標籤,一起參與開源貢獻\~