浏览器_Script 的 defer 和 async - zen0822/interview GitHub Wiki

Script 的 defer 和 async

开局一张图:

图解 deferf 和 async

Script 默认的行为

当浏览器加载 HTML 时遇到 <script>...</script> 标签,浏览器就不能继续构建 DOM。它必须立刻执行此脚本。对于外部脚本 <script src="..."></script> 也是一样的:浏览器必须等脚本下载完,并执行结束,之后才能继续处理剩余的页面。

这会导致两个重要的问题:

  1. 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本不能给它们添加事件等。
  2. 如果页面顶部有一个庞大的脚本,它会“阻塞页面”。在脚本下载并执行结束前,用户都不能看到页面内容:
<p>...content before script...</p>

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

<!-- 在脚本加载结束前都看不到下面的内容 -->
<p>...content after script...</p>

解决方法除了可以把脚本放在页面底部,此时,它可以访问到它上面的元素,并且不会阻塞页面显示内容。但是这样的方案绝非完美。例如:浏览器只有在下载完整的 HTML 文档后才会注意到脚本(并且开始下载它)。对于长的 HTML 文档来说,这样的延迟必须引起注意。

这里还有两个 <script> 属性可以解决我们的这个问题:deferasync

defer

defer 属性告诉浏览器它应该继续处理页面,并在“后台”下载脚本,然后等页面处理完成后才开始执行此脚本。

  • 具有 defer 属性的脚本不会阻塞页面的加载。
  • 具有 defer 属性的脚本总是要等到 DOM 解析完毕,但在 DOMContentLoaded 事件之前执行。

页面事件的生命周期:

  • DOMContentLoadedDOM 树解析完毕,完成之后才会触发 DOMContentLoaded,此时 GPU 还没开始根据 CSS 树开始绘画,我们可以在这个阶段使用 JS 去访问元素。
    • asyncdefer 的脚本可能还没有执行。
    • 图片及其他资源文件可能还在下载中。
  • load 事件在页面所有资源被加载完毕后触发,通常我们不会用到这个事件,因为我们不需要等那么久。window.load 只会执行第一次赋值的函数,无论是函数形式赋值还是标签内赋值 <body onload="alert('d');">
  • beforeunload 在用户即将离开页面时触发,它返回一个字符串,浏览器会向用户展示并询问这个字符串以确定是否离开。
  • unload 在用户已经离开时触发,我们在这个阶段仅可以做一些没有延迟的操作,由于种种限制,很少被使用。
  • document.readyState 表征页面的加载状态,可以在 readystatechange 中追踪页面的变化状态:
    • loading —— 页面正在加载中。
    • interactive —— 页面解析完毕,时间上和 DOMContentLoaded 同时发生,不过顺序在它之前。
    • complete —— 页面上的资源都已加载完毕,时间上和 window.onload 同时发生,不过顺序在他之前。

下面的例子证明上述情况:

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

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

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

<!-- 立即可见 -->
<p>...content after scripts...</p>
  1. 页面内容立即显示。
  2. DOMContentLoaded 在等待 defer 脚本动作的完成。它仅在脚本 (2) 下载且执行结束后才被触发。

脚本加载顺序

Defer 脚本保持他们的相对顺序,就像常规脚本一样。 所以,如果我们有一个长脚本在前,一个短脚本在后,那么后者就会等待前者。

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

短脚本先下载完成,但是后执行

浏览器解析页面找到 script 属性并并行下载它们,以提高性能。因此,在上面的实例中,两个脚本并行下载。small.js 可能会先下载完成。但是规范要求脚本按照文档顺序执行,因此它要等到 long.js 执行结束才会被执行。

defer 属性仅适用于外部脚本

defer 属性会忽略没有 src 属性的 <script> 脚本。

async

async 属性意味着脚本是完全独立的:

  • 页面不会等待异步脚本下载,它会继续处理页面并显示内容,但是会等待脚本执行
  • DOMContentLoadedasync 脚本不会彼此等待:
    • DOMContentLoaded 可能发生在异步脚本之前(此时异步脚本在页面加载完成后才加载完成)
    • DOMContentLoaded 也可能发生在异步脚本之后(此时异步脚本可能很短或者是从 HTTP 缓存中加载的)
  • 其他脚本不会等待 async 脚本加载完成,同样 async 脚本也不会等待其他脚本。

动态脚本(Dynamic scripts)

我们也可以使用 JavaScript 动态地添加脚本:

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

当脚本附加到文档 (*) 时,脚本就会开始加载:

默认情况下,动态脚本表现为“异步”行为。

这也就是说:

  • 它们不会等待其他内容,其他的内容也不会等待它们。
  • 先加载完成的脚本先运行(“加载优先” 顺序)

我们可以通过将 async 属性显示修改为 false 以将加载优先顺序修改为文档顺序(就像常规脚本一样)

⚠️ **GitHub.com Fallback** ⚠️