32

一个Vue引发的性能问题

 5 years ago
source link: https://www.yinchengli.com/2019/03/24/vue-performance-problem/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

笔者最近在一个Vue项目里面引入了一个动画库,但是发现性能有点异常,项目里面使用的CPU是在一个demo页面的3.5倍左右,我已经把项目里所有其它干扰的东西都给删掉了,但是CPU就是降不下去,如下图所示,正常范围是在2.1%左右波动:

muUVVvZ.png!web

但是引到项目里面就变成了7%左右波动:

A36F7v7.png!web

这个会不会是因为html嵌套太深导致Layout等计算复杂,所以CPU上升了呢,笔者尝试把DOM结构简单化,以及加上contain: strict等Layout隔离的方法,也是没有效果。所以只能是JS执行问题了,通过Chrome devtools的Performance可以研究这个问题。

如下图所示:

faMBfuM.png!web

上面密密麻麻的线都是requestAnimationFrame的回调,把它放大,然后查看一个回调,比较一下demo页面和Vue页面的不同之处,如下图所示:

r22iayE.png!web

这里明显可以看出区别,demo.html每个回调的执行时间是0.3ms左右,而Vue项目的回调执行时间达到了0.8ms左右,快接近3倍,且调用栈深了很多。多出来的这些东西是什么呢?仔细一看:

BFNvyy6.png!web

这些东西是Vue里面的,也就是Vue里面setter,部分回调里面还包含了Vue里的getter:

mIfMVvJ.png!web

这个时候恍然大悟,因为Vue里面重写了变量的getter/setter,导致获取某个属性或者改写某个属性的时间变长,导致CPU上升。造成Vue重写的原因是因为在代码里面把动画库的变量当成了组件里this的属性,如下代码所示:

import Player from 'player.js';
 
export default {
  data: {
    return {
      player: new Player()
    };
  }
};

然后Vue就会遍历这个player对象,给所有的属性都加上setter/getter,如下控制打印所示:

vMV3qai.png!web

这里的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%左右,快接近一半,如下图所示:

eIzYJbr.png!web

查看Performance里面的setter的调用栈就没有了,如下图所示:

BvQFjqZ.png!web

但是CPU仍然是demo页面的两倍(2%和4%),这个时候继续查看调用栈,发现是一个ji的函数调用时间一个是另一个的两倍:

E7Vrm2A.png!web

这两个函数点过去Source面板看代码的时候确认是两个一样的函数,这里唯一的区别可能在于demo.html用的是压缩的代码,而本地的项目是未压缩,如果打包压缩一下,放到测试环境,可以看到CPU时间基本就差不多了:

2uQ3qi3.png!web

压缩代码里面会把多条语句合并为一条语句应该也会提升点性能。

最后,本文并不是说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%:

iUZB3mM.png!web

如果我让它睡眠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%左右波动,如下图所示:

V3QRNrR.png!web

是不是挺有趣的呢

Post Views: 1


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK