Web性能优化:timers

Posted Cubic

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Web性能优化:timers相关的知识,希望对你有一定的参考价值。

众所周知,浏览器的javascript引擎是基于单线程的。我们可以通过setTimeout来创建异步任务,实现防抖(debounce)、节流(throttle)、缓存续期(renew)等操作。

通过setTimeoutclearTimeout,我们很快就可以实现一个缓存管理器:

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);
}
}
}

然后在一个频繁使用缓存的场景下,我们惊奇地发现,缓存处理占用的时间占据了总处理时间的一半,时间都耗费在了setTimeoutclearTimeout上,这个开销是非常大的,甚至让我们使用缓存的意义都不那么大了。

Web性能优化:timers

原因分析

首先,我们可以大概了解一下setTimeoutclearTimeout的原理。

根据html规范,可以知道,每次调用setTimeout都会发生以下步骤:

  • 做一些准备工作
  • 生成一个handle,值为一个大于0的整数,用于标识当前设置的timeout
  • 把这个handle加入到激活的定时器列表(list of active timers)中,并关联一个任务
  • 返回这个handle,等前面的timer执行完后执行这个任务

每次clearTimeout都会从激活的定时器列表中去掉对应的handle

这里就存在了一个问题,每次新增和销毁timer,都会涉及一个列表的操作,而且由于不同的timer之间是有执行顺序的,所以这个列表中的元素必然会相互影响。所以不论这个列表底层是如何维护的,频繁操作肯定会有一些问题。

于是,我用ChromePerformance工具做了个实验。

用上面实现的cache,连续调用1000次缓存续期:

for (let i = 0; i < 1000; i += 1) {
cache.hit(key, 1000);
}

得到如下结果:

Web性能优化:timers

从图中可以看到,CPU的消耗有了一个高峰,setTimeoutclearTimeout耗时近50ms,如果同时有别的操作,则很容易造成页面卡顿。而实际上,上面的代码啥都没干,从结果来看,把整个循环去掉,也不会产生什么区别,这就意味着,timer的消耗非常不划算,有很大的优化空间。

Web性能优化:timers

解决方案

我们无法对浏览器底层进行优化,但是我们可以从实现上尽量避免产生无效的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);
}
}
}

改造后效果如下:

Web性能优化:timers

可以看到,CPU 几乎没有什么波动,总耗时只有1ms,差距非常明显。

延伸

除了缓存管理的场景,我们在处理频繁触发的事件(如scrollmousemove)时,也会经常用到timer来减少处理的频率。这时debounce的实现就会存在和上面类似的潜在问题。如果在大型SPA中大量使用多timer并发的方式,就有可能导致页面性能大量损耗在timer上,而出现卡顿或者其他问题。

我们从监控上发现过少量奇怪的错误:

Uncaught RangeError: Failed to execute 'setTimeout' on 'Window': Too many PausableObjects

Google了一下,并没有找到有效的信息,只是发现PausableObjectChromium中实现的一个类,大概是用于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的主要内容,如果未能解决你的问题,请参考以下文章

Web项目开发性能优化解决方案

web开发性能优化---SEO优化篇

web性能优化

技能篇—web性能优化

web性能优化之---JavaScript中的无阻塞加载性能优化方案

Web 性能优化文档及代码编辑器相关的新提案