使用 web 技术栈开发的应用,往往需要使用定时器实现一些轮询或定时更新之类的操作。也许你思考过,倘若放任页面自身的定时任务自由运行,是否会对用户的系统资源消耗产生不可忽略的开销。事实上,chromium 内核的确具有对 js 定时器的触发限流策略,这些策略或多或少的会对应用本身的运行时序有不容忽视的影响。在此,我们简单的了解下相关机制,以帮助更好的组织、实现页面功能。
连锁定时器(Chained Timer)
所谓“连锁定时器”,指的是在某个定时任务回调中触发的下一个定时器。根据这个概念,setInterval
将总是被认为是连锁定时器,其连锁次数由其触发次数决定。以下是使用 setInterval
、setTimeout
且被认定为连锁定时器的例子:
let chainCount = 0;
setInterval(() => {
chainCount++;
console.log(`This is number ${chainCount} in the chain`);
}, 500);
let chainCount = 0;
function setTimeoutChain() {
setTimeout(() => {
chainCount++;
console.log(`This is number ${chainCount} in the chain`);
setTimeoutChain();
}, 500);
}
最小间隔限流
当满足以下任意一种条件时,浏览器将对页面定时器进行最小间隔限流:
- 页面可见;
- 页面在最近 30 秒内有播放出声音。
其限制方式为:如果定时器延迟时间如果小于 4ms,且当前的连锁执行次数 >= 5,则将定时器的触发最小间隔设置为 4ms。可以在页面中快速测试一下:
let p = performance.now();
setInterval(() => {
let t = performance.now() - p;
p = performance.now();
console.log(t);
}, 1);
上例中,我们采用 performance.now()
获取高精度时间,便于观察精确的触发间隔。定时器应该以 1ms 的间隔运行,并打印出离上一次运行的时间间隔。运行结果如下:
定时器的前 4 次触发基本都是接近 1ms;从第 5 次开始,触发间隔被限制为接近 4ms。结合该策略的触发条件,几乎任何激活的页面中的定时器,都会受到影响。可以认为:浏览器中,定时器通常无法以低于 4ms 的间隔持续、稳定运行。
常规限流
当定时器不处于以上提及的“最小间隔限流”策略下、且页面满足以下任意一种条件时,浏览器将对定时器进行常规限流:
- 定时器连锁次数 < 5;
- 页面不可见且其不可见时间不超过 5min;
- 页面正在使用 WebRTC 进行实时流传输。
在这种情况下,浏览器将会每秒检查一次,并且将这一秒内的定时任务批量触发一次。同样的,我们可以通过以下一段代码在页面中简单的验证;
window.addEventListener('visibilitychange', () => {
console.log('page visible?', document.visibilityState);
});
let p = performance.now();
setInterval(() => {
let t = performance.now() - p;
p = performance.now();
console.log(t);
}, 300);
上例中,我们通过 visibilitychange
监听页面可见状态的变化;稍后,我们将会以切换浏览器 Tab 的方式控制页面可见状态,并观察定时器触发频率。运行结果如下:
尽管我们设置的定时间隔是 300ms,但在页面不可见后,实际的触发间隔变成了 1000ms!因此,如果我们有一些小于 1s 触发间隔的定时任务,那么它们很可能在页面隐藏时变得不可靠。
激进限制
当定时器不处于以上的“最小间隔限流”或“常规限流”策略下、且页面满足以下所有条件时,浏览器将对定时器进行激进限流:
- 页面已不可见超过 5min;
- 定时器连锁次数 >= 5;
- 页面未在使用 WebRTC 进行实时流传输。
在这种情况下,浏览器将会每分钟检查一次,并且将这一分钟内的所有定时任务批量触发一次。以下是一段验证代码:
window.addEventListener('visibilitychange', () => {
console.log('page visible?', document.visibilityState);
});
function startTimer(id, interval) {
let p = performance.now();
setInterval(() => {
let t = performance.now() - p;
p = performance.now();
console.log(`${id}: ${t}ms`);
}, interval);
}
startTimer('t1', 10_000);
startTimer('t2', 30_000);
startTimer('t3', 50_000);
运行结果如下:
可以看到,t1
t2
t3
这三个定时器在页面长期不可见后,集中在每分钟一次的时间点同时触发,较原定频率大大降低,同时也有可能因任务密集触发执行出现进程间歇繁忙。
总结
为了节省应用的资源占用,降低 CPU 使用率从而降低能耗、提高电池使用频率,chromium 针对定时器可谓是煞费苦心,设计了以上几种不同的限流触发策略。作为前端开发人员,我们可能会遇到由此带来的一系列问题,例如:基于定时器的动画或业务逻辑无法按预期节奏执行、某一批不同的定时任务总是集中在一个时间点触发、浏览器自动化测试任务出现非预期的结果,等等。
这种影响,并不仅仅出现在 web 应用中,同样也出现在使用 chromium 内核的桌面应用中,例如 electron。由于桌面应用的长期运行特性,该限流策略带来的影响往往会放大,变得难以忍受(例如某些轮询场景,不能及时获取到最新消息等等)。在 electron 应用中,我们可以通过给窗口指定 backgroundThrottling: false
显式禁掉其的定时器限流策略,或者通过应用全局的参数设置 --disable-background-timer-throttling
禁掉所有窗口的该限制。当然,这样做会失去原策略带来的降低系统消耗的收益。如果确实想“既要”“又要”,那可能需要做针对性的改造以规避了。
References
- https://blog.chromium.org/2020/11/tab-throttling-and-more-performance.html
- https://docs.google.com/document/d/11FhKHRcABGS4SWPFGwoL6g0ALMqrFKapCk5ZTKKupEk/view
- https://developer.chrome.com/blog/background_tabs
- https://developer.chrome.com/blog/timer-throttling-in-chrome-88
- https://www.electronjs.org/docs/latest/api/command-line#commandlineappendswitchswitch-value
- https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions