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