一个Vue引发的性能问题

234 阅读5分钟
原文链接: www.yinchengli.com

笔者最近在一个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);
  // 从源码也可以看到,可以把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%左右波动,如下图所示:

是不是挺有趣的呢

Post Views: 248