一个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%左右波动,以下图所示:

是否是挺有趣的呢

相关文章
相关标签/搜索