一個Vue引起的性能問題

筆者最近在一個Vue項目裏面引入了一個動畫庫,可是發現性能有點異常,項目裏面使用的CPU是在一個demo頁面的3.5倍左右,我已經把項目裏全部其它干擾的東西都給刪掉了,可是CPU就是降不下去,以下圖所示,正常範圍是在2.1%左右波動:javascript

可是引到項目裏面就變成了7%左右波動:html

這個會不會是由於html嵌套太深致使Layout等計算複雜,因此CPU上升了呢,筆者嘗試把DOM結構簡單化,以及加上contain: strict等Layout隔離的方法,也是沒有效果。因此只能是JS執行問題了,經過Chrome devtools的Performance能夠研究這個問題。java

以下圖所示:react

上面密密麻麻的線都是requestAnimationFrame的回調,把它放大,而後查看一個回調,比較一下demo頁面和Vue頁面的不一樣之處,以下圖所示:async

這裏明顯能夠看出區別,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,而設置了的,將會是用「(...)」代替,而後等到你去點的時候再去獲取它當前的值顯示出來。ui

從Vue源碼裏面能夠看到,Vue會對成員變量進行defineProperty設置setter和getter:

// 代碼有所刪減
function defineReactive$$1 (obj, key, val) {
  var dep = new Dep();
  
  var property = Object.getOwnPropertyDescriptor(obj, key);
  // 從源碼也能夠看到,能夠把obj的configurable置爲false,Vue便不會設置getter和setter
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;

  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 {
    };
  }
};複製代碼

(補充:從Vue的源碼也能夠看到,把object的configurable屬性置成false也能夠解決問題。)

這個時候CPU從7%降到了4%左右,快接近一半,以下圖所示:

查看Performance裏面的setter的調用棧就沒有了,以下圖所示:

可是CPU仍然是demo頁面的兩倍(2%和4%),這個時候繼續查看調用棧,發現是一個ji的函數調用時間一個是另外一個的兩倍:

這兩個函數點過去Source面板看代碼的時候確認是兩個同樣的函數,這裏惟一的區別可能在於demo.html用的是壓縮的代碼,而本地的項目是未壓縮,若是打包壓縮一下,放到測試環境,能夠看到CPU時間基本就差很少了:

壓縮代碼裏面會把多條語句合併爲一條語句應該也會提高點性能。

綜上,本文並非說Vue的實現有問題,只是須要注意setter/getter對性能的影響,特別是在一個動畫的回調裏面,通常狀況下對於一次性的操做影響幾乎是可忽略的,應該不須要關心這個問題。另外,只是設置動畫裏面的setter/getter也不必定會使CPU一會兒就升上去了,還要看你在setter/getter裏面幹了些啥,在Vue裏面能夠看到它的調用棧是比較深的,可能內部須要判斷的東西比較多。


另外這個研究讓想起了一個有趣的問題,如何讓CPU使用率維持在50%?若是我寫一個for寫循環,那麼CPU使用率必定是100%(它把一個核跑滿了),以下代碼所示:

let now = Date.now();
// 跑個50s
while (Date.now() - now < 50000);複製代碼

這個時候CPU使用率就是100%:

若是我讓它睡眠50ms,而後再幹50ms,反覆交替,以下代碼所示:

function sleep (time) {
  return new Promise(resolve => {
    setTimeout(resolve, time);
  });
}
let now = Date.now();
async function start () {
  while (Date.now() - now < 50000) {
    // 睡50ms
    await sleep(50);
    let current = Date.now();
    // 幹50ms
    while (Date.now() - current < 50);
  }
  console.log('end');
}
start();複製代碼

這個時候CPU使用率就會在50%左右波動,以下圖所示:

是否是挺有趣的呢

相關文章
相關標籤/搜索