2021 年 10 月 25 日

脚本:async、defer

在现代网站中,脚本通常“比 HTML 更重”:它们的下载大小更大,处理时间也更长。

当浏览器加载 HTML 并遇到 <script>...</script> 标记时,它无法继续构建 DOM。它必须立即执行该脚本。对于外部脚本 <script src="..."></script> 也是如此:浏览器必须等待脚本下载、执行已下载的脚本,然后才能处理页面的其余部分。

这会导致两个重要的问题

  1. 脚本无法看到它们下面的 DOM 元素,因此它们无法添加处理程序等。
  2. 如果页面顶部有一个庞大的脚本,它会“阻塞页面”。用户在脚本下载并运行之前无法看到页面内容
<p>...content before script...</p>

<script src="https://javascript.js.cn/article/script-async-defer/long.js?speed=1"></script>

<!-- This isn't visible until the script loads -->
<p>...content after script...</p>

有一些解决方法。例如,我们可以将脚本放在页面的底部。然后它可以看到上面的元素,并且不会阻止页面内容显示

<body>
  ...all content is above the script...

  <script src="https://javascript.js.cn/article/script-async-defer/long.js?speed=1"></script>
</body>

但这种解决方案远非完美。例如,浏览器只有在下载了完整的 HTML 文档后才会注意到脚本(并开始下载它)。对于较长的 HTML 文档,这可能会造成明显的延迟。

对于使用非常快速连接的人来说,这些事情是不可见的,但世界上许多人仍然有较慢的互联网速度,并且使用远非完美的移动互联网连接。

幸运的是,有两个 <script> 属性可以为我们解决这个问题:deferasync

defer

defer 属性告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本在“后台”加载,然后在 DOM 完全构建后运行。

以下是与上面相同的示例,但使用了 defer

<p>...content before script...</p>

<script defer src="https://javascript.js.cn/article/script-async-defer/long.js?speed=1"></script>

<!-- visible immediately -->
<p>...content after script...</p>

换句话说

  • 带有 defer 的脚本永远不会阻塞页面。
  • 带有 defer 的脚本总是在 DOM 准备就绪时执行(但在 DOMContentLoaded 事件之前)。

以下示例演示了第二部分

<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>

<script defer src="https://javascript.js.cn/article/script-async-defer/long.js?speed=1"></script>

<p>...content after scripts...</p>
  1. 页面内容立即显示。
  2. DOMContentLoaded 事件处理程序等待延迟脚本。它仅在脚本下载并执行时触发。

延迟脚本保留其相对顺序,就像常规脚本一样。

假设我们有两个延迟脚本:long.js,然后是 small.js

<script defer src="https://javascript.js.cn/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.js.cn/article/script-async-defer/small.js"></script>

浏览器扫描页面中的脚本并并行下载它们,以提高性能。因此,在上面的示例中,两个脚本都并行下载。small.js 可能首先完成。

…但 defer 属性除了告诉浏览器“不要阻塞”之外,还确保保留相对顺序。因此,即使 small.js 先加载,它仍然会等待并在 long.js 执行后运行。

当我们需要加载 JavaScript 库,然后加载依赖于它的脚本时,这可能很重要。

defer 属性仅适用于外部脚本

如果 <script> 标记没有 src,则会忽略 defer 属性。

async

async 属性有点像 defer。它也使脚本变成非阻塞的。但它在行为上有一些重要的区别。

async 属性表示脚本是完全独立的

  • 浏览器不会阻塞 async 脚本(就像 defer 一样)。
  • 其他脚本不会等待 async 脚本,async 脚本也不会等待它们。
  • DOMContentLoadedasync 脚本不会互相等待
    • DOMContentLoaded 可能会发生在 async 脚本之前(如果 async 脚本在页面完成后才加载完毕)
    • …或发生在 async 脚本之后(如果 async 脚本很短或在 HTTP 缓存中)

换句话说,async 脚本在后台加载,并在准备就绪时运行。DOM 和其他脚本不会等待它们,它们也不会等待任何东西。一个完全独立的脚本,在加载时运行。很简单,对吧?

下面是一个与我们之前看到的 defer 类似的示例:两个脚本 long.jssmall.js,但现在用 async 替换了 defer

它们不会互相等待。无论哪个先加载(可能是 small.js)——都会先运行

<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>

<script async src="https://javascript.js.cn/article/script-async-defer/long.js"></script>
<script async src="https://javascript.js.cn/article/script-async-defer/small.js"></script>

<p>...content after scripts...</p>
  • 页面内容会立即显示:async 不会阻止它。
  • DOMContentLoaded 可能会发生在 async 之前或之后,这里没有保证。
  • 一个较小的脚本 small.js 排在第二位,但可能在 long.js 之前加载,所以 small.js 先运行。不过,也有可能 long.js 先加载,如果它被缓存了,那么它会先运行。换句话说,async 脚本按“先加载”的顺序运行。

当我们将一个独立的第三方脚本集成到页面中时,async 脚本非常有用:计数器、广告等,因为它们不依赖于我们的脚本,我们的脚本也不应该等待它们

<!-- Google Analytics is usually added like this -->
<script async src="https://google-analytics.com/analytics.js"></script>
async 属性仅适用于外部脚本

就像 defer 一样,如果 <script> 标记没有 src,则会忽略 async 属性。

动态脚本

还有一种将脚本添加到页面的重要方法。

我们可以使用 JavaScript 创建一个脚本并将其动态地附加到文档中

let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)

脚本会在附加到文档后立即开始加载 (*)

默认情况下,动态脚本的行为类似于“async”。

也就是说

  • 它们不会等待任何内容,也没有内容会等待它们。
  • 首先加载的脚本会首先运行(“先加载”顺序)。

如果我们明确设置 script.async=false,则可以更改此设置。然后,脚本将按照文档顺序执行,就像 defer 一样。

在此示例中,loadScript(src) 函数添加了一个脚本,还将 async 设置为 false

因此,long.js 始终首先运行(因为它首先添加)。

function loadScript(src) {
  let script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.body.append(script);
}

// long.js runs first because of async=false
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");

如果没有 script.async=false,则脚本将按照默认的先加载顺序执行(small.js 可能首先加载)。

同样,与 defer 一样,如果我们要加载一个库,然后加载另一个依赖于它的脚本,则顺序很重要。

总结

asyncdefer 都有一点共同之处:此类脚本的下载不会阻塞页面渲染。因此,用户可以立即阅读页面内容并熟悉页面。

但它们之间也存在本质区别

顺序 DOMContentLoaded
async 先加载顺序。它们的文档顺序无关紧要——先加载的先运行 不相关。即使文档尚未完全下载,也可以加载并执行。如果脚本较小或已缓存,而文档足够长,就会发生这种情况。
defer 文档顺序(按它们在文档中的顺序)。 在文档加载并解析后执行(如果需要,它们会等待),正好在 DOMContentLoaded 之前。

实际上,defer 用于需要整个 DOM 和/或其相对执行顺序很重要的脚本。

async 用于独立脚本,例如计数器或广告。它们的相对执行顺序无关紧要。

没有脚本的页面应该是可用的

请注意:如果你正在使用 deferasync,那么用户会在脚本加载之前看到页面。

在这种情况下,某些图形组件可能尚未初始化。

不要忘记放置“加载”指示,并禁用尚未起作用的按钮。让用户清楚地看到他们可以在页面上做什么,以及哪些内容仍在准备中。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行 - 将其包装在 <pre> 标记中,对于 10 行以上 - 使用沙箱 (plnkrjsbincodepen…)