前端技术文档和网站数据接口使用指南详情
网站开发中有关html、css、js等前端技术文档和易助科技网网站数据接口使用文档及相关资料下载!

Scripts 脚本的 async 和 defer 属性详解

来源:易助科技网浏览量:15收藏

概述


在当今网站中,Scripts 脚本通常比 HTML 更庞大:它们的体积更大,下载和处理时间也更长。

当浏览器加载 HTML 并遇到 <script>...</script> 标记,它无法继续构建 DOM。它必须立即执行脚本。外部脚本 <script src=“…”></script> 也会发生同样的情况:浏览器必须等待脚本下载,执行下载的脚本,然后才能处理页面的其余部分。这导致了两个严重要问题:


1. 脚本看不到下面的DOM元素,因此无法添加处理程序等。

2. 如果页面顶部有一个庞大的脚本,它就会“阻塞页面”。在下载并运行之前,用户无法查看相关页面内容。


<p>...script之前的内容...</p>

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

<!-- 在加载脚本之前,这是不可见的 -->
<p>...script之后的内容...</p>


有一些变通办法来应对这种情况。例如,我们可以在页面的底部放置一个脚本。然后它可以看到上面的元素,并且不会阻止页面内容显示。


<body>
  ...所有内容都在脚本之上...

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


但这种解决方案远非完美。例如,浏览器只有在下载了完整的 HTML 文档后才会注意到脚本(开始下载和运行)。对于长HTML文档,这可能会有一个较长的延迟。对于网速很快的用户来说,这些影响并不明显,但对于网速较慢以及使用的移动互联网的用户,这种解决方案会面临很多问题。

幸运的是,有两个<script>属性为我们解决了这个问题:defer 和 async。



defer


defer属性告诉浏览器不要等待脚本。相反,浏览器将继续处理HTML,构建DOM。该脚本“在后台”加载,然后在DOM完全构建时运行。以下是与上面相同的示例,但使用了defer:


<p>...脚本之前的内容...</p>

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

<!-- 内容马上可见 -->
<p>...脚本之后的内容...</p>


也就是说:

1. 具有 defer 的脚本从不阻塞页面。

2. 具有 defer 的脚本总是在 DOM 就绪时(但在 DOMContentLoaded 事件之前)执行。

以下示例演示了 DOMContentLoaded 事件情况:


<p>...脚本之前的内容...</p>

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

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

<p>...脚本之后的内容...</p>


1. 页面内容立即显示。

2. DOMContentLoaded 事件处理程序等待延迟的脚本。它只在脚本下载并执行时触发。


与常规脚本一样,延迟脚本保持其相对顺序。


例如,我们有两个延迟的脚本:long.js 和 small.js:


<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>


浏览器在页面上扫描脚本并并行下载,以提高性能。因此,在上面的示例中,两个脚本都是并行下载的。small.js可能是第一名。

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

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


💡 defer 属性仅适用于外部脚本,如果<script>标记没有 src,则会忽略 defer 属性。



async


async 属性有点像 defer。它还使脚本不受阻塞。但它在行为上有着重要的差异。async 属性表示脚本是完全独立的:


1. 浏览器不会阻止 async 脚本(如 defer)。

2. 其他脚本不等待 async 脚本,async 脚本也不等待它们。

3. DOMContentLoaded 和异步脚本互不等待:DOMContentLoaded 可能发生在 async 脚本之前(如果 async 脚本在页面完成后完成加载),或在 async 脚本之后(如果 async 脚本很短或在 HTTP 缓存中)


换句话说,async 脚本在后台加载,并在准备好后运行。DOM 和其他脚本不等待它们,也不等待任何东西。加载时运行的完全独立的脚本。这里有一个类似于我们在 defer 中看到的例子:两个脚本 long.js 和 small.js,但现在使用async 而不是 defer。

他们不会互相等待。先加载的内容(可能是 small.js)先运行:


<p>...脚本之前的内容...</p>

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

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

<p>...脚本之后的内容...</p>


1. 页面内容立即显示:async 不会阻止它。

2. DOMContentLoaded 可能发生在 async 之前和之后,这里没有保证。

3. 较小的脚本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 属性。



Dynamic scripts


还有一种更重要的方法可以将脚本添加到页面中。我们可以创建一个脚本,并使用JavaScript动态地将其附加到文档中:


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


脚本在附加到文档(*)后立即开始加载。默认情况下,动态脚本表现为“异步”。即:

1. 他们什么都不等,什么都没有等着他们。

2. 先加载的脚本–先运行(“先加载”的顺序)。

如果我们显式设置 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 一样,如果我们想加载一个库,然后加载另一个依赖它的脚本,那么顺序很重要。



总结


async 和 defer 都有一个共同点:下载这样的脚本不会阻止页面呈现。因此,用户可以阅读页面内容并立即熟悉页面。但它们之间也有本质的区别:


OrderDOMContentLoaded
asyncLoad-first order. 他们的文档顺序无关紧要——先加载的先运行无关的可能在文档尚未完全下载时加载并执行。如果脚本很小或缓存,并且文档足够长,就会发生这种情况。
deferDocument order (as they go in the document).在加载和解析文档后执行(如果需要,请等待),正好在DOMContentLoaded之前执行。


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

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


💡 请注意:


如果您使用的是 defer 或 async,那么用户将在加载脚本之前看到页面。

在这种情况下,一些图形组件可能还没有初始化。

不要忘记放置“加载”指示和禁用尚未起作用的按钮。让用户清楚地看到他可以在页面上做什么,以及还有什么准备。