为什么setTimeout和setInterval的实际延时比设置的时间长(译文)

点击查看英文原文

延时比设置的时间长的原因

  setTimeout/setInterval延时超出预期时间是由多种原因造成的。常见的原因归结如下:

超时被限制>=4ms

  在现代浏览器中,由于回调嵌套(嵌套级别至少达到一定深度)或者在一定数量的连续时间间隔后触发连续调用时,setTimeout()/setInterval()的调用周期最小值被限制为4ms。例如:

1
2
3
4
5
function cb() {
f();
setTimeout(cb, 0);
}
setTimeout(cb, 0);

1
setInterval(f, 0);

  在Chrome和Firefox中,从第5次连续回调开始显示回调周期;Safari从第6次连续回调开始限制;Edge在第三次连续调用时开始限制。Gecko从版本 56开始对setInterval()调用采取与上述限制策略(它已经像这样处理setTimeout()调用;具体参见下文)。
  从历史上看,一些浏览器实施这种限制行为略微不同(比如Firefox)——setInterval()调用从任何地方开始或者setTimeout()嵌套调用级别达到一定深度。
  为了在现代浏览器中实现0ms延时,可以参考这篇文章使用window.postMessage()实现。

非活动标签页中的超时被限制>=1000ms

  为了减小后台标签页中的加载量(同时减小电池消耗),非活动标签页中超时触发频率每秒(1000ms)不超过一次。
  Firefox从版本 5(详见bug 633421,通过dom.min_background_timeout_value可以修改1000ms这一常量)开始实施这一限制行为,Chrome从版本 11开始实施该行为。
  在适用于安卓的Firefox浏览器中,从Firefox 14的bug 736602开始,后台标签页的超时触发间隔被设置为15分钟,并且后台标签可以被完全卸载。
  注:当后台标签页使用Web Audio API AudioContext播放音频时,Firefox 50不对后台标签页中进行限制。从Firefox 51开始,一旦后台标签页中出现了AudioContext,即使页面没有播放音频,Firefox也不限制该页面。

对追踪脚本的超时限制

  从Firefox 55开始,追踪脚本(比如Google Analytics,任何Firefox通过其TP表识别的脚本URL)受到了进一步的限制。追踪脚本在前台页面运行时,其最小时间间隔仍然设置为4ms。但是当追踪脚本在后台标签页运行时,其最小延时被限制为10000ms或者10s,这将在文档首次加载之后30s生效。
  控制这一限制行为的字段如下:

  1. dom.min_tracking_timeout_value: 4
  2. dom.min_tracking_background_timeout_value: 10000
  3. dom.timeout.tracking_throttling_delay: 30000

晚超时

  除了浏览器的限制之外,当页面(或者操作系统/浏览器本身)忙于其他任务时,超时也会晚触发。很重要的一点是,对应的回调函数或者代码片段不能被执行直到调用setTimeout()的线程结束。比如:

1
2
3
4
5
function foo() {
console.log('foo has been called');
}
setTimeout(foo, 0);
console.log('After setTimeout');

  要写入控制台的结果:

1
2
After setTimeout
foo has been called

  这是因为即使setTimeout被调用时延时为0,它也会被放置在一个执行队列中并且计划在下一个时机被执行而不是立即被执行。当前正在被执行的代码必须在执行队列中的函数被执行前执行完成,因此最终的执行顺序可能不符合预期。