笔者最近在一个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的属性,如下代码所示:
1 2 3 4 5 6 7 8 9 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 代码有所删减 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里面的成员属性,而是把它弄到外面去,如下代码所示:
1 2 3 4 5 6 7 8 9 |
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%(它把一个核跑满了),如下代码所示:
1 2 3 |
let now = Date.now(); // 跑个50s while (Date.now() - now < 50000); |
这个时候CPU使用率就是100%:
如果我让它睡眠50ms,然后再干50ms,反复交替,如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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%左右波动,如下图所示:
是不是挺有趣的呢