一個Vue引發的效能問題
筆者最近在一個Vue專案裡面引入了一個動畫庫,但是發現效能有點異常,專案裡面使用的CPU是在一個demo頁面的3.5倍左右,我已經把專案裡所有其它干擾的東西都給刪掉了,但是CPU就是降不下去,如下圖所示,正常範圍是在2.1%左右波動:
但是引到專案裡面就變成了7%左右波動:
這個會不會是因為html巢狀太深導致Layout等計算複雜,所以CPU上升了呢,筆者嘗試把DOM結構簡單化,以及加上contain: strict等Layout隔離的方法,也是沒有效果。所以只能是JS執行問題了,通過Chrome devtools的Performance可以研究這個問題。
如下圖所示:
上面密密麻麻的線都是requestAnimationFrame的回撥,把它放大,然後檢視一個回撥,比較一下demo頁面和Vue頁面的不同之處,如下圖所示:
這裡明顯可以看出區別,demo.html每個回撥的執行時間是0.3ms左右,而Vue專案的回撥執行時間達到了0.8ms左右,快接近3倍,且呼叫棧深了很多。多出來的這些東西是什麼呢?仔細一看:
這些東西是Vue裡面的,也就是Vue裡面setter,部分回撥裡面還包含了Vue裡的getter:
這個時候恍然大悟,因為Vue裡面重寫了變數的getter/setter,導致獲取某個屬性或者改寫某個屬性的時間變長,導致CPU上升。造成Vue重寫的原因是因為在程式碼裡面把動畫庫的變數當成了元件裡this的屬性,如下程式碼所示:
import Player from 'player.js'; export default { data: { return { player: new Player() }; } };複製程式碼
然後Vue就會遍歷這個player物件,給所有的屬性都加上setter/getter,如下控制列印所示:
這裡的Ir.set就是上面Performance裡面的截圖,也就是這個導致了設定Ii變數變慢了。這裡我們注意到一個細節,Chrome控制檯會直接列印沒有覆蓋setter/getter的Object,而設定了的,將會是用“(...)”代替,然後等到你去點的時候再去獲取它當前的值顯示出來。
從Vue原始碼裡面可以看到,Vue會對成員變數進行defineProperty設定setter和getter:
// 程式碼有所刪減 function defineReactive$$1 (obj, key, val) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; if (setter) { setter.call(obj, newVal); } else { val = newVal; } dep.notify(); } }); }複製程式碼
以便使用者設定值的時候做一些通知,從而達到資料驅動的目的。但同時也有可能造成效能問題,在這個例子裡面是增加了0.3ms左右的呼叫時間。實際上這個時間幾乎是可以忽略的,但是由於這個例子裡面需要執行在requestAnimationFrame裡面,1s呼叫60次,比較頻繁,原本的時候也就才0.2ms,而現在由於這個setter/getter,增加了0.3ms,比正常時間多了一倍多,所以CPU就升上去了。
知道原因就能解決問題了,現在的解決方式是不要把這個player變數當成this裡面的成員屬性,而是把它弄到外面去,如下程式碼所示:
import Player from 'player.js'; let player = new Player(); export default { data: { return { }; } };複製程式碼
這個時候CPU從7%降到了4%左右,快接近一半,如下圖所示:
檢視Performance裡面的setter的呼叫棧就沒有了,如下圖所示:
但是CPU仍然是demo頁面的兩倍(2%和4%),這個時候繼續檢視呼叫棧,發現是一個ji的函式呼叫時間一個是另一個的兩倍:
這兩個函式點過去Source面板看程式碼的時候確認是兩個一樣的函式,這裡唯一的區別可能在於demo.html用的是壓縮的程式碼,而本地的專案是未壓縮,如果打包壓縮一下,放到測試環境,可以看到CPU時間基本就差不多了:
壓縮程式碼裡面會把多條語句合併為一條語句應該也會提升點效能。
最後,本文並不是說Vue的實現有問題,只是需要注意setter/getter對效能的影響,特別是在一個動畫的回撥裡面,一般情況下對於一次性的操作影響幾乎是可忽略的,應該不需要關心這個問題。另外,只是設定動畫裡面的setter/getter也不一定會使CPU一下子就升上去了,還要看你在setter/getter裡面幹了些啥,在Vue裡面可以看到它的呼叫棧是比較深的,可能內部需要判斷的東西比較多。