狙殺頁面卡頓 —— Performance 工具指北

狙殺頁面卡頓 —— Performance 工具指北

來自專欄餓了么前端176 人贊了文章

今天介紹下 Chrome dev tools 家族的一個小兄弟,它在 Chrome 57 之前叫作「Timeline」,而現在換了個更長的馬甲 —— 「Performance」,畢竟名字要「長~~~~~~~~~」更能吸引注意。

也許你曾不經意啟動過這個工具,看見裡面五顏六色的圖表後和我一樣頭暈目眩。但今天介紹完它後,我相信你能像熟悉瑞士軍刀一樣熟悉它。

這個面板叫做「Performance」,不過名字里也沒有指明是什麼性能。既然是 Chrome 的調試工具,那應該和頁面有關係,我們就從頁面性能聊起。

什麼會影響你的頁面性能

近年來,WEB 開發者們為縮短用戶等待時間做出了一系列方案,以期「短益求短」。比如用 PWA 緩存更多可用的離線資源,讓網頁應用打開更快;藉助 WebAssembly 規範縮小資源體積,提高執行效率。這些方案分別著眼於網路鏈路,前端資源處理速度等維度上,致力提高用戶體驗。

作為 WEB 開發者,我感受到跟頁面性能掛鉤比較深的幾個維度是:網路鏈路、伺服器資源、前端資源渲染效率、用戶端硬體。

網路鏈路

網路鏈路往往是頁面性能的扼要之處,域名解析、交換機、路由器、網路服務提供商、內容分發網路、伺服器,鏈路上的節點出問題或響應過慢都會有不好的體驗。

伺服器資源

在 HTTP 的大環境下,所有請求最終都要伺服器來處理,伺服器爸爸處理不當無法響應或響應過慢也會直接影響頁面與用戶的互動。

前端資源渲染

瀏覽器獲取所需 HTML、CSS、腳本、圖片等靜態資源,繪製首屏呈現給用戶的過程;或用戶與頁面交互後,瀏覽器重新計算需要呈現的內容,然後重新繪製的過程。這些過程的處理效率也是影響性能的重要因素。

用戶硬體

發起網路請求,解析網路響應,頁面渲染繪製等過程都需要消耗計算機硬體資源。所以計算機資源,特別是 CPU 和 GPU 資源短缺時(比如打顯卡殺手類的遊戲),也會影響頁面性能。

當然,以上的維度不是劃線而治的,它們更多是犬牙交錯的關係。例如在渲染過程中瀏覽器反應很慢,有可能是腳本寫得太爛遭遇性能瓶頸,也有可能是顯卡殺手遊戲佔用了過多計算機資源;又如在分析前端資源渲染時,往往要結合網路瀑布圖分析資源的獲取時間,因為渲染頁也是個動態的過程,有些關鍵資源需要等待,有些則可以在渲染的同時載入。

為什麼祭出 Performance

Chrome 的開發者工具各有自己的側重點,如 Network 工具的瀑布圖有著資源拉取順序的詳細信息,它的側重點在於分析網路鏈路。而 Performance 工具的側重點則在於前端渲染過程,它擁有幀率條形圖、CPU 使用率面積圖、資源瀑布圖、主線程火焰圖、事件總覽等模塊,它們和渲染息息相關,善用它們可以清晰地觀察整個渲染階段。不過,你不必糾結上面提到的模塊名,因為在接下來的篇幅里,我會一一介紹他們。

用正確的姿勢啟動 Performance

打開 Performance

首先我們打開 Chrome 匿名窗口,在匿名環境下,瀏覽器不會有額外的插件、用戶特性、緩存等影響實驗可重複性的因素。接著啟動開發者工具,如果你的窗口寬度足夠,可以在頂端標籤欄的第 5 欄看到 Performance,寬度不夠則可以通過右上的 >> 按鈕點開更多標籤,找到它。

此時我們看到的是 Performance 的默認引導頁面。其中第一句提示語所對應的操作是立即開始記錄當前頁面發生的所有事件,點擊停止按鈕才會停止記錄。第二句對應的操作則是重載頁面並記錄事件,工具會自動在頁面載入完畢處於可交互狀態時停止記錄,兩者最終都會生成報告(生成報告需要大量運算,會花費一些時間)。

現在,工具已準備好,可以開始分析頁面了。

簡單頁面分析

首先我們分析一個簡單頁面從空白頁面到渲染完畢的過程。本文所有示例頁面都放在下面的倉庫里,通過命令克隆並切換到倉庫根目錄:

git clone git@github.com:pobusama/chrome-preformance-use-demo.git && cd chrome-preformance-use-demo

接著安裝依賴包:npm i

最後啟動示例頁面:npm run demo1

由於很難把握頁面開始渲染的時機,我們通過第二種 reload 方式收集渲染數據,將 beforeunload -> unload -> Send Request(第一個資源請求) -> load 的過程都記錄下來。

在工具自動停止記錄後,我們得到了這樣一份報告:

圖中划出的 4 個區域分別是:

1:控制面板,用來控制工具的特性。「Network」與「CPU」:分別限制網路和計算資源,模擬不同終端環境,可以更容易觀測到性能瓶頸。「Disable JavaScript samples」選項開啟會使工具忽略記錄 JS 的調用棧,這個我們之後會再提到。打開「Enable advanced paint instrumentation」則會詳細記錄某些渲染事件的細節,這個功能我們在了解這些事件後再聊。

2:概覽面板,其中有描述幀率(FPS)、CPU 使用率、網路資源情況的 3 個圖表。幀率是描繪每秒鐘渲染多少幀圖像的指標,幀率越高則在觀感上更流暢。網路情況是以瀑布圖的形式呈現,圖中可以觀察到各資源的載入時間與順序。CPU 使用率面積圖的其實是一張連續的堆積柱狀圖(下面 CPU 面積圖放大版為示意圖,數據非嚴謹對應):

其縱軸是 CPU 使用率,橫軸是時間,不同的顏色代表著不同的事件類型,其中:

  • 藍色:載入(Loading)事件
  • 黃色:腳本運算(Scripting)事件
  • 紫色:渲染(Rendering)事件
  • 綠色:繪製(Painting)事件
  • 灰色:其他(Other)
  • 閑置:瀏覽器空閑

舉例來說,示意圖的第一列:總 CPU 使用率為 18,載入事件(藍色)和腳本運算事件(黃色)各佔了一半(9)。隨著時間增加,腳本運算事件的 CPU 使用率逐漸增加,而載入事件的使用率在 600ms 左右降為 0;另一方面渲染事件(紫色)的使用率先升後降,在 1100ms 左右降為 0。整張圖可以清晰地體現哪個時間段什麼事件佔據 CPU 多少比例的使用率。

3:線程面板,用以觀察細節事件,在概覽面板縮小觀察範圍可以看到線程圖的細節。其中主線程火焰圖是用來分析渲染性能的主要圖表。不同於「正常」火焰圖,這裡展示的火焰圖是倒置的,即最上層是父級函數或應用,越往下則調用棧越淺,最底層的一小格(如果時間維度拉得不夠長,看起來像是一小豎線)表示的是函數調用棧頂層。

默認情況下火焰圖會記錄已執行 JS 程序調用棧中的每層函數(精確到單個函數的粒度),非常詳細。而開啟「Disable JS Samples」後,火焰圖只會精確到事件級別(調用某個 JS 文件中的函數是一個事件),忽略該事件下的所有 JS 函數調用棧。

此外,幀線程時序圖(Frames)和網路瀑布圖(Network)可以從時間維度分別查看繪製出的頁面和資源載入情況。

4:詳情面板。前面已經多次提到事件,我想如果再不解釋可能要被寄刀片了。Performance 工具中,所有的記錄的最細粒度就是事件。這裡的事件不是指 JS 中的事件,而是一個抽象概念,我們打開主線程火焰圖,隨意點擊一個方塊,就可以在詳情面板里看到該事件的詳情,包括事件名、事件耗時、發起者等信息。舉幾個例子:Parse HTML 是一種 Loading 事件(藍色),它表示在在事件時間內,Chrome 正在執行其 HTML 解析演算法;Event 是一種 Scripting 事件(黃色),它表示正在執行 JS 事件(例如 click);Paint 是一種繪製事件(綠色),表示 Chrome 將合成的圖層繪製出來。

以下是一些常見事件,有個印象就好,由於每次做性能分析必會跟它們打交道,我們想不記住他們也難。

詳情面板還有非常重要的一部分就是事件耗時餅狀圖,它列出了你選擇的時間段內,不同類型事件(載入、腳本運算、渲染、繪製、其他事件、發獃:) )所佔的比例和耗費的時間。分析耗時佔比與分析 CPU 面積圖有相通的意義 —— 到底是哪種事件消耗了大量算力和時間,導致了性能瓶頸。

至此,我們掃了一遍 Performance 工具的主要功能,雖然沒有面面俱到,但足以開啟性能分析之旅。接下來我們分析一個稍微複雜些的動畫頁面,真正理解使用這些圖表數據如何定位性能問題。

嘮叨一下瀏覽器渲染過程

知曉瀏覽器的渲染過程對我們理解分析十分重要,這裡簡要介紹一下瀏覽器渲染的過程:

當渲染首屏時,瀏覽器分別解析 HTML 與 CSS 文件,生成文檔對象模型(DOM)與 樣式表對象模型(CSSOM);合併 DOM 與 CSSOM 成為渲染樹(Render Tree);計算樣式( Style);計算每個節點在屏幕中的精確位置與大小(Layout);將渲染樹按照上一步計算出的位置繪製到圖層上(Paint);渲染引擎合成所有圖層最終使人眼可見(Composite Layers)。

如果改變頁面布局,則是先通過 JS 更新 DOM 再經歷計算樣式到合成圖像這個過程。如果僅僅是非幾何變化(顏色、visibility、transform),則可以跳過布局步驟。

動畫分析

有了上面這些準備,相信你已開始摩拳擦掌了。我們在示例倉庫下跑另外一個 Demo:cd chrome-preformance-use-demo && npm run demo2

初始狀態下,10個小方塊會分別上下勻速運動,碰到瀏覽器邊界後原路返回。「Add 10」是增加 10 個這樣的小方塊,「Substract 10」是減少 10 個,「Stop/Start」暫停/開啟所有小方塊的運動,「Optimize/Unoptimize」優化/取消優化動畫。

瀏覽器是怎麼繪製一幀動畫的

在默認狀態下,我們點擊左上角的圓記錄事件,幾秒後我們可以點擊 Performance 中的 Stop 產生分析數據。在概覽面板中我們看到在渡過最初的幾百毫秒後,CPU 面積圖中各種事件佔比按固定周期變化,我們點取其中一小段觀察,在主線程圖中可看到一段一段類似事件組。觀察幾組事件後,我們發現,這些事件組的組成都是:10 個 Recalculate Style + Layout 的循環 -> Update Layer Tree -> Paint -> Composite Layers。我們知道,Composite Layers 事件之後,意味著人眼可見新的頁面圖層,也就是說這樣一組事件過後,一幀畫面繪製在眼前。

我們點開主線程火焰圖的上一欄「Framse」,發現 Composite Layers 事件後不久的虛線處就是下一幀畫面出現的節點,這側面證實了上面的結論。

當瓶頸出現時

目前的動畫看著沒什麼毛病,我們點擊 20 次「Add 10」按鈕,增加方塊數,可以看到動畫出現了明顯的卡頓,如果還不感覺卡頓,說明你的計算機性能已經擊敗了全國 99% 的用戶(或者,呃...你可能要去醫院看眼睛了),這時你可以在控制面板里降低 CPU 算力。好了,我們再次記錄性能數據:

我們看到報告中有多處醒目的紅色,包括幀率圖上的大紅杠、主線程圖中的小角標。

再次按照之前的思路,查看主線程的細節,我們發現在 app.update 函數下發生的 Recalculate Style 和 Layout 事件都出現了警告,提示性能瓶頸的原因可能是強制重排。進入 js 文件查看詳細代碼,在左欄可以看到消耗了大量時間的代碼行呈深黃色,那麼這些代碼就很有可能是癥結所在。

注意:自動計算代碼行運算時間需要取消勾選控制面版的「Disable JavaScript Samples」選項。

那麼,這行代碼到底有什麼問題呢,重排又是什麼呢?

再談重排與重繪

簡而言之,重排(reflow)和重繪(repaint)都是改變頁面樣式的步驟。重排步驟包括 Recalculate Style、Layout、Update Layer Tree 等渲染類型事件,重繪步驟包括 Paint 和 Composite Layers 這些繪製類型事件。重排之後必然會造成重繪,而造成重繪的操作不一定會造成重排。下面列出了一些造成重排或重繪的常見操作,更多操作可以參閱 csstriggers

由於計算布局需要大量時間,重排的開銷遠大於重繪,在達到相同效果的情況下,我們需要盡量避免重排。舉個例子,如果 display: none 和 visibility: hidden 都能滿足需求,那麼後者更優。

解決瓶頸

再回頭看一下我們的動畫 Demo,在 performance 的詳情面板中,餅圖顯示動畫的繪製過程中渲染(重排)佔據的大部分的比重,結合代碼我們發現原因:循環中多次在剛給 DOM 更新樣式位置後,立即通過 offsetTop 獲取 DOM 位置。這樣的操作會強制啟動重排,因為瀏覽器並不清楚上一個循環內 DOM 有沒有改變位置,必須立即重新布局才能計算 DOM 位置。別急,你可能已經注意到了,我們還有一個「Optimize」按鈕。

針對這個問題,我們的優化方案是將 offsetTop 替換成 style.top,後者雖然取的是上一幀動畫的元素位置,但並不影響計算下一幀動畫位置,省去了重排獲取位置的過程,減少了不必要的重排。

注意:本示例中,還有一處優化是非 Optimize 的情況下就做了的,就是通過 requestAnimationFrame 函數將一幀循環內所有的樣式改動(m.style.top = pos + px;)累計在下一次繪製時統一處理。這樣做除了優化了樣式的寫操作,還讓我們更清楚地觀察到 offsetTop 讀操作這個問題的現象。

我們對比一下優化前後的報告:

首先從餅圖和 CPU 面積圖看,Rendering 事件佔比下滑,Painting 事件佔比上升。而從幀率圖和 frames 線程圖中分別可看到,幀率明顯上升,一幀圖像的繪製時間明顯下降,意味著動畫流暢性大幅提高,優化目的已達到。再看細節,我們發現繪製一幀動畫的事件組中,app.update 函數里沒有了 Recalculate style 和 Layout 事件,整個函數執行時間因此顯著下降,證明我們的優化方案起了作用。

回顧與小記

本次關於性能工具的討論,我們從影響頁面性能的因素談起,隨之引出了 Performance 工具擅長的維度 —— 前端資源渲染。接著,我們了解了 Performance 工具 4 個主要面板:控制、預覽、線程、詳情,還有幾個實用的圖表:幀率條形圖、CPU 面積圖、主線程火焰圖、幀線程時序圖、事件耗時餅狀圖。然後運用它們定位了一個性能問題,並著手解決了該問題。

然而,真實環境下的性能問題更加複雜,在熟練運用 Performance 的同時我們需要將影響性能的因素熟記於心,這部分的知識和經驗需要在實戰中長期積累。提升 WEB 性能是個有趣的話題,谷歌 WEB 開發者網站中有很多優質的博客對此展開討論,不過,瀏覽器里沒有魔法,拿起 Performance 愉快地玩耍吧~

參考

Get Started With Analyzing Runtime Performance?

developers.google.com

Rendering Tools?

developers.google.com

Render Tree Construction?

developers.google.com

Avoid Forced Synchronous Layouts?

developers.google.com

如何讀懂火焰圖? - 阮一峰的網路日誌?

www.ruanyifeng.com圖標網頁性能管理詳解 - 阮一峰的網路日誌?

www.ruanyifeng.com圖標
推薦閱讀:

七個前端必須知道的性能優化點,你知道多少?
【SNF-A】Angular 增加 Hammer.js 的 Lazy Loading 支持
前端性能優化(一)用一張圖說明載入優化
前端性能優化:細說瀏覽器渲染的重排與重繪
網頁性能優化的一些方法

TAG:前端開發 | 前端性能優化 | GoogleChrome |