實例解析:《奇趣百科》性能優化
奇趣百科年后進行了一次大改版, 不論是內容還是程序架構上,改版使用Vue.js的MVVM的理念令開發過程加速了不少。但是改版后卻出現了明顯的性能問題,出現了比較明顯的頁面卡頓,因此我們又專門做了一次性能優化。本文主要介紹Chrome DevTools中的Timeline Profils等工具的使用方法。
1. 組件粒度加粗
首先最先想到的就是用Timeline看一下:
Frames情況還算正常, 但是注意到內存占用已經快17MB了, 相對于改版前的12MB是明顯偏高的(由于篇幅關系就不上圖了), 那么我們繼續來追查內存相關的,使用Chrome開發者工具的Profiles看一下當前的內存占用情況:
打開Chrome開發者工具 -> 點擊 Profiles 控制板 -> 選中 Take Heap Snapshot -> 點擊 Take Snapshot
查看了一下, 發現listItemHead, listItemImg , listItemMeta 和 listTuwen 組件對象分別都各有9個或者10個(9個是因為業務邏輯問題)。
Vue.js支持組件系統,因此,為了提高復用性,我把一個卡片定義為一個組件,而這個組件又由若干個組件組成。
卡片本身是listTuwen組件, 該組件包括了三個組件: listItemHead, listItemImg 和 listItemMeta。
接下來我們來研究一下snapshot表格中相應的列分別代表什么。
一個對象有兩種形式來持有內存:
-
直接擁有
-
間接引用
分別對應 snapshot 中的Shallow Size和Retained Size
Shallow Size
Shallow Size代表了對象直接持有的內存大小。一個標準的JS對象通常會持有用于描述自身邏輯和存儲直接值(屬性值)的內存。 通常情況下應該只有字符串和數組類型可能擁有一個較大的Shallow Size。
Retained Size
Retained Size代表了當前對象所引用的其他對象占用的內存大小. 當當前對象被銷毀時, 這一部分的內存會被釋放。
奇趣百科的首頁觸底加載一共可以加載30次一共300張卡片,我們加載30次完畢后與剛進入首頁的情況進行對比:
內存占用已經飆升到了70MB了, 我們切換到 Comparison 視圖(紅框), 并選擇 Snapshot 1(紅框)。
#New 一列說明了三個組件的對象都增加了290個(289的是因為業務邏輯), Size Delta 一列說明了三個組件的對象各自增加了快7M的內存, 加起來就是20+MB了。
因此我們可以得出這樣一個結論: 同一頁面中大量被重用的組件盡量不要嵌套其他組件, 不然內存占用會隨著組件的增多而快速上升。
可以看到一個Vue組件對象內部引用了大量的其他對象, 包括directives, watchers 等, 還有一些系列的 getter 和 setter 方法。
解決辦法就是卡片內部不使用組件, 一個卡片就只有自身這個組件, 采用其他方法來提高代碼的復用性。
最后我們來對比優化后的結果:
觸底加載完畢,300張卡片占用內存40M左右,雖然listTuwen組件的對象占用的內用大了很多,但是總體下降了40%,優化效果很明顯。
移除視窗外的不必要的DOM
頁面上DOM的數目越少,占用內存就越少,性能也就越好,這是很容易得出來的結論。
參照手機淘寶搜索結果頁的做法, 我們可以把視窗外的不可見的卡片移除掉, 當這些卡片滾動回到窗口內(或者滾動位置接近到窗口的某個像素值)后再插入顯示.列表的卡片數目保持在一個恒定的值,而不是直線增長下去.
奇趣百科線上的代碼可以看到,我們的卡片數目保持在30個,也就是 DOM 的數據是恒定的,不會隨著頁面越滾動到下面越多。
接下來我們看一下內存情況驗證我們這樣做的效果:
優化前后的 Timeline 工具的內存曲線:
內存曲線都成鋸齒形狀,有觸發垃圾回收,但是優化后的曲線上升的斜率比優化前少了2°,即內存上升速度慢了。
另外是優化前后的 Profiles 對比('DOM'作為關鍵字進行篩選):
內存優化后少了近10M,并且DOM的數據減少了10倍,內存使用下降25%。
圖片懶加載
進行這一個優化點之前,我們先來科普一下Timeline這個控制板。
最好在瀏覽器隱身模式下使用,禁用一切無關插件,因為插件也會占用內存,影響測試結果。如果需要記錄網絡請求的話, 最好把瀏覽器緩存也禁用掉。
網上盜的一張圖(出處):
有三種模式可以切換關注點:
-
Events: 顯示所有事件的記錄
-
Frames: 顯示頁面渲染的幀數
-
Memory: 顯示頁面的內存情況
這里我們重點關注 Frames 模式。
頁 面的每一幀內容都是GPU繪制出來的,它的最高繪制頻率受限于顯示器的刷新頻率,大多數情況下最高的繪制平率只能是每秒60幀(frames per second, 即fps),對應于顯示器的60Hz。因此在頁面性能的測試中,60fps是一個非常重要的指標,越接近越好。
這里說到了一個常量 -- 屏幕刷新頻率60Hz。
60Hz 和60fps有什么關系?沒有任何關系。fps代表GPU渲染畫面的頻率,Hz代表顯示器刷新屏幕的頻率。一幅靜態圖片,你可以說這副圖片的fps是0幀 /秒,但絕對不能說此時屏幕的刷新率是0Hz,也就是說刷新率不隨圖像內容的變化而變化。游戲也好瀏覽器也好,我們談到掉幀,是指GPU渲染畫面頻率降 低。比如跌落到30fps甚至20fps,但因為視覺暫留原理,我們看到的畫面仍然是運動和連貫的。
Frames 模式模式中的 Frames 就是"幀". "一幀"(Frames模式下的一條柱子)代表了顯示器為了在一幀()內展現內容所要完成的工作,包括執行JavaScript,處理事件,更新DOM,改變樣式和布局還有繪制頁面.
在Frame視圖中有兩條貫穿該視圖的橫線,分別標識出60FPS和30FPS的基準。
注意到有些柱子有一部分是空白的或者是灰色的,分別代表:
-
空白: 空閑時間
-
灰色: 沒有被記錄的活動,可以理解成是瀏覽器內部c++的一些工作,這部分和前端的js以及渲染沒什么關系.
現在來看一下項目的 Frames 情況:
看到超出 60fps 的柱子還是挺多的, 而且都是柱子的大部分顏色都是綠色的。
先來說明一下柱子顏色的含義:
-
藍色: 網絡和HTML解析
-
黃色: JavaScript 腳本運行
-
紫色: 樣式重計算和布局 ( Layout , Recaculate Style, Update Layer tree)
-
綠色: 繪制和合成 ( Paint , Composite Layers)
所以我們大部分時間話費在繪制上了. 我們再選取一些比較高的柱子, 看看都有什么特點:
我們看到有一些空的綠色色塊和實心的綠色色塊,是這樣的:
繪制分兩步走: 畫和渲染
-
畫: 這包括了一些系列你想要畫出來的東西, 而這些是由元素上的CSS而得來的。
-
渲染: 逐條分析上一步中你想要"畫"的東西, 利用 GPU 來組合填充這些東西的實際像素。
這一部分我翻譯得很爛,因為我自己也不太懂具體的意思,所以大家可以看看原文 → About the green bars
而 Painting 包括了這些事件:
事件 描述 Composite Layers Chrome的渲染引擎完成圖片層合并時觸發 Image Decode 一個圖片資源完成解碼后觸發 Image Resize 一個圖片被修改尺寸后觸發 Paint 合并后的層被繪制到對應顯示區域后觸發。
根 據我的觀察, 我發現比較高的綠色柱子一般都包括多個Image Decode事件, 圖片加載回來而觸發了這個事件, 進而產生了大量的Rasterize Paint事件, 所以我猜測, 把圖片都分開加載, 不要一次性就加載10張圖片, 這樣就得把事件分散, 高的柱子拆成多個矮的柱子, 這樣就能進一步提升流暢度。
圖片懶加載是怎么實現的就不細說了, 類似的效果可以參照淘寶首頁。
最后我們來看一下優化后的 Timeline :
這個情況已經是達到比較理想的狀態了, 實際操作也比較流暢。
但是值得一提的是, 最后 PM 并沒有采納這一步的優化方式,因為體驗過后,PM認為圖片懶加載反而會讓用戶覺得卡頓, 并不是實際滑動上的卡頓, 而是整體體驗上的卡。所以最后從用戶體驗的角度出發, 我們的優化方案并沒有采用圖片懶加載。