Web性能优化:timers
Posted Cubic
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Web性能优化:timers相关的知识,希望对你有一定的参考价值。
众所周知,浏览器的javascript引擎是基于单线程的。我们可以通过setTimeout来创建异步任务,实现防抖(debounce)、节流(throttle)、缓存续期(renew)等操作。
通过setTimeout和clearTimeout,我们很快就可以实现一个缓存管理器:
class Cache {
// ...
put(key, value, lifetime) {
const cache = { value };
this.data[key] = cache;
this.hit(key, lifetime);
}
hit(key, lifetime) {
const cache = this.data[key];
if (cache) {
clearTimeout(cache.timer);
cache.timer = setTimeout(this.clear, lifetime, key);
}
}
}
然后在一个频繁使用缓存的场景下,我们惊奇地发现,缓存处理占用的时间占据了总处理时间的一半,时间都耗费在了setTimeout和clearTimeout上,这个开销是非常大的,甚至让我们使用缓存的意义都不那么大了。
原因分析
首先,我们可以大概了解一下setTimeout和clearTimeout的原理。
根据html规范,可以知道,每次调用setTimeout都会发生以下步骤:
-
做一些准备工作 -
生成一个handle,值为一个大于0的整数,用于标识当前设置的timeout -
把这个handle加入到激活的定时器列表(list of active timers)中,并关联一个任务 -
返回这个handle,等前面的timer执行完后执行这个任务
每次clearTimeout都会从激活的定时器列表中去掉对应的handle。
这里就存在了一个问题,每次新增和销毁timer,都会涉及一个列表的操作,而且由于不同的timer之间是有执行顺序的,所以这个列表中的元素必然会相互影响。所以不论这个列表底层是如何维护的,频繁操作肯定会有一些问题。
于是,我用Chrome的Performance工具做了个实验。
用上面实现的cache,连续调用1000次缓存续期:
for (let i = 0; i < 1000; i += 1) {
cache.hit(key, 1000);
}
得到如下结果:
从图中可以看到,CPU的消耗有了一个高峰,setTimeout和clearTimeout耗时近50ms,如果同时有别的操作,则很容易造成页面卡顿。而实际上,上面的代码啥都没干,从结果来看,把整个循环去掉,也不会产生什么区别,这就意味着,timer的消耗非常不划算,有很大的优化空间。
解决方案
我们无法对浏览器底层进行优化,但是我们可以从实现上尽量避免产生无效的timer:
-
每次只产生一个timer,同时保存续期后实际的到期时间 -
等当前timer到期后,重新计算超时时间,如果有需要,再重新创建一个timer
这个过程是连续的(consecutive),而不再是有很多个timer在并行,所以逻辑上更清晰了,效率也明显提升。
代码改造如下:
class Cache {
// ...
put(key, value, lifetime) {
const cache = { value };
this.data[key] = cache;
this.hit(key, lifetime);
}
hit(key, lifetime) {
const cache = this.data[key];
if (cache) {
cache.expire = Date.now() + lifetime;
if (!cache.timer) this.checkLater(key);
}
}
check(key) {
const cache = this.data[key];
if (cache) {
if (cache.expire < Date.now()) {
delete this.data[key];
} else {
this.checkLater();
}
}
}
checkLater(key) {
const cache = this.data[key];
if (cache) {
cache.timer = setTimeout(this.check, cache.expire - Date.now(), key);
}
}
}
改造后效果如下:
可以看到,CPU 几乎没有什么波动,总耗时只有1ms,差距非常明显。
延伸
除了缓存管理的场景,我们在处理频繁触发的事件(如scroll、mousemove)时,也会经常用到timer来减少处理的频率。这时debounce的实现就会存在和上面类似的潜在问题。如果在大型SPA中大量使用多timer并发的方式,就有可能导致页面性能大量损耗在timer上,而出现卡顿或者其他问题。
我们从监控上发现过少量奇怪的错误:
Uncaught RangeError: Failed to execute 'setTimeout' on 'Window': Too many PausableObjects
Google了一下,并没有找到有效的信息,只是发现PausableObject是Chromium中实现的一个类,大概是用于JavaScript线程挂起的时候,暂停当前执行的任务,包括所有的timer。
更多信息:https://groups.google.com/a/chromium.org/forum/#!topic/platform-architecture-dev/vxyodocHoMc
最后根据目前了解的情况和网上获得的极少信息,猜想这个问题出现的原因是,当前注册的timer已经多到影响每一帧的渲染或是达到了浏览器内部的某些限制了,所以无法再创建更多的timer。所以就联想到可能跟debounce的实现方式上的缺陷有关。
总结
1. timer的性能并不好,不适合大量使用。
2. 我们可以结合JavaScript是单线程的特点,从设计上避免大量使用timer的问题。
以上是关于Web性能优化:timers的主要内容,如果未能解决你的问题,请参考以下文章