使用 setTimeout 加速 window.onload

setTimeout() 中的 JavaScript 代码不一定会延迟 onload 事件。 考虑以下测试用例:

<!DOCTYPE html>
<meta charset="utf-8">
<title>setTimeout() and window.onload</title>
<script>

    setTimeout(function() {
        document.body.style.background = 'red';
    }, 3000);

    window.onload = function() {
        document.body.style.background = 'green';
    };

</script>

打开此文档时会发生以下情况:

  • 浏览器的 HTML 解析器会执行它的操作,但一旦遇到开始的 <script> 标记就会停止。
  • <script> 元素的内容被执行。 setTimeout 将导致一些其他代码在 3 秒内运行,并且事件处理程序绑定到 window.onload
  • HTML 解析器继续解析文档直到结束。
  • 由于此页面上没有其他资源,因此一旦解析器完成,就会触发 onload 事件。
  • 调用 window.onload 事件处理程序。 该文档获得绿色背景。
  • 大约 3 秒后,setTimeout 函数启动。文档变成红色背景。

在此示例中,使用 setTimeout 不会延迟 onload 事件。 注意,其他场景不一定如此! 例如,考虑以下文档:

<!DOCTYPE html>
<meta charset="utf-8">
<title>setTimeout() and window.onload</title>
<img src="image.png">
<script>

    setTimeout(function() {
        document.body.style.background = 'red';
    }, 3000);

    window.onload = function() {
        document.body.style.background = 'green';
    };

</script>

如各位所见,我们已将图像添加到文档中。 (当然,这可能是阻止 onload 的任何其他资源。)onload 事件不会在图像完全加载之前触发。

这给了我们一个稍微不同的结果:

  1. 浏览器的 HTML 解析器开始解析。
  2. 一旦 <img> 元素被解析,浏览器就会开始下载图像。
  3. 解析器一遇到开始的 <script> 标记就会停止。
  4. <script> 元素的内容被执行。 setTimeout 将导致一些其他代码在 3 秒内运行,并且事件处理程序绑定到 window.onload
  5. HTML 解析器继续解析文档直到结束。
  6. 一旦图像完全加载, onload 事件就会触发。
  7. 调用 window.onload 事件处理程序。 该文档获得绿色背景。

如大家所见,此列表中缺少一个步骤,仅仅是因为无法准确预测它应该去哪里。 setTimeout 中的代码将在步骤 4 后 3 秒执行,我们知道很多。 但是取决于下载图像需要多长时间,这可能是在加载之前或之后。

假设图片加载需要 1 秒。 这意味着 onload 事件也会在大约一秒钟后触发。 两秒后,setTimeout 中的代码将最终执行。

如果图像加载需要 5 秒,onload 事件将在此期间再次延迟,因此 setTimeout 中的代码将在 onload 之前执行。

如果加载图像需要大约 3 秒,即 setTimeout 的延迟参数,会发生什么情况? onload 会在 setTimeout 中的代码之前、期间或之后触发吗?

要回答这个问题,我们需要了解 JavaScript 是单线程的。(关于这一点,可以参考我们的 setTimeout究竟做了什么 这篇文章) 如果在浏览器准备好触发 onload 时有一些代码正在运行(来自内联脚本或来自 setTimeout 内部),则浏览器将必须“等待”直到脚本完成才能处理 onload。 同样,如果我们使用 setTimeout 下载资源,并且它们在 onload 触发之前进入下载队列,那么加载这些资源将(仍然)延迟 onload

换句话说,使用 setTimeout 并不能保证在所有情况下都能加速 onload 事件。 但大多数时候,它会。


这有用吗?

这意味着我们可以使用 setTimeout(fn, 0) 模式来防止延迟 onload 事件,从而导致感知加载/渲染时间减少。 当然,这种技术只能用于非依赖的脚本。

我认为跟踪脚本就是一个很好的例子。 其中大多数动态地将新的 <script> 元素插入到 DOM 中。 如各位所知,每个 DOM 操作都会带来一定的性能损失。 例如,默认的 Google Analytics 代码会在遇到代码段时立即修改 DOM,从而延迟 onload 事件。

这是 Google Analytics 片段的修改版本,使用 setTimeout 来防止延迟加载:

var _gaq = [['_setAccount', 'UA-XXXXX-X'], ['_trackPageview']];
    setTimeout(function() {
        var g = document.createElement('script'),
            s = document.scripts[0];
        g.src = 'https://ssl.google-analytics.com/ga.js';
        s.parentNode.insertBefore(g, s);
    }, 0);

一些注意事项

如前所述,此技术不能用于依赖的脚本。 很难预测 setTimeout(fn, 0) 中的 fn 何时会执行。 如果你有两个或三个这样的结构,就无法判断它们的执行顺序。

另请注意 ,setTimeout 中函数的 this 绑定会自动覆盖到全局窗口对象。 这可能会破坏依赖于 this 引用的脚本。

请注意 ,HTML5 规范允许的最小 setTimeout 超时值为 4 毫秒。 较小的值(如 0)应限制在 4 毫秒。 我们可以在此处测试哪些浏览器遵循这方面的规范。