Blog.

setTimeout(fn, 0) 并非延时 0ms 也不是 4ms❗❗

LiuuY

本文翻译自:Ivan Akulov

我们经常看到文章说,setTimeout(fn, 0) 并非延时 0ms,而是至少有 4ms的延时。

现在这个说法已经不正确了,至少对于最新版的 Chrome 和 Safari 来说。

让我们从头讲起。

  1. HTML 标准文档里面说,超过 5 层以上的嵌套 setTimeout 需要延迟 4ms 执行。这是为了防止低端硬件 CPU 过载。(注意是「嵌套」setTimeout 需要延迟,文档中并没有要求非嵌套 setTimeout 需要延迟!)。

译者注:这里的「嵌套」指的是 setTimeout 中嵌套 setTimeout:

  setTimeout(() => {
    setTimeout(() => {
      ...
    }, 0)
  }, 0)
  1. 浏览器都遵守这个标准。然而在 2022 年,Chromium 注意到,把需要延迟的嵌套层级从 5 增加到 7,可以增加 10% 的性能测试分数。

https://chromestatus.com/feature/5710690097561600

  1. 这听起来是为了性能测试分数而做的优化,但是这个「性能测试」是模拟了 TodoMVC,所以这个测试成绩是可以反应到真实用户体验的。

经过一些讨论后(原本要增加到 100?Increased nesting threshold for SetTimeout(0)),把需要延迟的嵌套层级增加到 15。

然而,这个修改并没有发布:(

  1. Safari 也将需要延迟的嵌套层级从 5 增加到 10。

https://sourcegraph.com/github.com/WebKit/WebKit/-/commit/786e3e0b252e38fb01c8db97a94d52cb0f57891e?visible=6

  1. HTML 文档中指表述了「嵌套」setTimeout,然而直到 2022 年,Chrome 和 Safari 都将把非嵌套 setTimeout(..., 0) 延迟 1ms!

这是 2006 年引入的逻辑,在第一版 Chrome 发布之前、更在在 Webkit 和 Blink 这两个内核分叉之前。

  1. 这个 1ms 延迟并非 HTML 标准文档中要求的,同时很可能对于性能有负面影响。所以 Chromium 在 2014 年就尝试修复这个问题(https://bugs.chromium.org/p/chromium/issues/detail?id=402694)。

然而,用了 8 年时间才修复并发布,原因可能是没时间和没测试?

  1. 尽管 Chromium (https://chromestatus.com/feature/4889002157015040) 和 Safari (https://bugs.webkit.org/show_bug.cgi?id=221124) 都在 2022 年移除了这个延迟,Firefox 从来都没有加过这个延迟。

  2. 所以,当我们调用 setTimeout(fn, 0) 时,实际发生的是:

  • 对于「非嵌套」setTimeout(fn, 0),在所有浏览器都会立即执行。

  • 对于「嵌套」setTimeout(fn, 0),超过 5 层(Firefox、Chrome)、10 层(Safari)会延迟 4ms。

    我们可以在这里测试:https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified

    注意,「立即执行」,指的是将回调函数 fn,放入 Task Queue 的最后一个。在 fn 执行前,

    a. 执行所有的 Microtasks(Promise、MutationObserver 等等)。

    b. 执行 Task Queue 中之前加入的任务。

    只有 Microtasks Queue 和 Task Queue 都为空的时候,fn 才会真的立即执行。

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)