defer 和 async 的区别

1. 先记住结论

deferasync 都可以让外部脚本在下载时不阻塞 HTML 解析,但它们的执行时机不同:

  • defer:等 HTML 解析完成后,按照脚本在文档中的顺序执行。
  • async:脚本下载完成后尽快执行,不保证执行顺序。

对于 defer,更准确的执行条件是:

HTML 已经解析完成、脚本已经下载完成,并且已经轮到该脚本按文档顺序执行。

因此:

  • 脚本先下载完:等待 HTML 解析完成。
  • HTML 先解析完:等待脚本下载完成。

2. defer 的完整执行时机

浏览器开始解析 HTML
遇到 defer 脚本,开始并行下载,继续解析 HTML
HTML 解析完成
等待 defer 脚本下载完成,并按文档顺序执行
触发 DOMContentLoaded
等待图片、视频、iframe 等其他资源加载完成
触发 window.load

也就是:

HTML 解析完成 → defer 脚本执行完毕 → DOMContentLoaded → window.load

defer 脚本执行时,完整的 DOM 已经构建完成,所以可以直接访问页面中的 DOM 元素。

3. defer、async 和普通脚本对比

写法下载行为执行时机执行顺序对 HTML 解析的影响
<script src="a.js">遇到脚本后下载下载完成后立即执行按文档顺序下载和执行都会暂停解析
<script async src="a.js">与 HTML 解析并行下载完成后尽快执行不保证下载不阻塞,执行时可能暂停解析
<script defer src="a.js">与 HTML 解析并行HTML 解析完成后、DOMContentLoaded 之前按文档顺序不会暂停 HTML 解析

可以简单理解为:

  • 普通脚本:遇到就停下来执行
  • async谁先下载完,谁先执行
  • defer先解析完 HTML,再排队执行

4. 多个 defer 脚本的执行顺序

<script defer src="a.js"></script>
<script defer src="b.js"></script>
<script defer src="c.js"></script>

即使 b.jsc.jsa.js 更早下载完成,执行顺序仍然是:

a.js → b.js → c.js → DOMContentLoaded

因此,当 b.js 依赖 a.js 时,可以使用 defer 保证依赖顺序。async 不适合这种场景。

5. load 会早于 defer 执行完吗?

正常情况下不会。

浏览器必须先执行完 defer 脚本,才会触发 DOMContentLoaded;而 window.load 又发生在 DOMContentLoaded 之后。因此,window.load 不可能在 defer 脚本的同步代码执行完之前触发。

但需要区分:

  • defer 脚本本身执行完毕。
  • defer 脚本启动的异步任务执行完毕。
// defer.js
console.log('defer 脚本开始执行');

setTimeout(() => {
  console.log('异步回调可能晚于 window.load');
}, 5000);

console.log('defer 脚本执行完毕');

这里的 defer.js 已经执行完毕,只是它创建的定时器回调还没有执行。浏览器不会为了这个异步回调而推迟 DOMContentLoadedwindow.load

6. 适用场景

defer

适合普通业务脚本,尤其是:

  • 需要操作完整 DOM。
  • 脚本之间存在依赖关系。
  • 希望脚本下载不阻塞 HTML 解析。

async

适合与页面其他代码没有依赖关系的独立脚本,例如:

  • 数据统计。
  • 广告脚本。
  • 独立的第三方 SDK。

7. 常见易错点

defer 对内联脚本不起作用

defer 主要用于带有 src 的外部经典脚本,下面的 defer 不会产生推迟效果:

<script defer>
  console.log('仍然立即执行');
</script>

type="module" 默认具有类似 defer 的行为

模块脚本默认不会阻塞 HTML 解析,并且会在文档解析完成后执行:

<script type="module" src="app.js"></script>

通常不需要再为模块脚本添加 defer。如果添加 async,它会在依赖模块下载完成后尽快执行,不再等待文档解析完成。

8. 面试回答模板

defer 脚本会与 HTML 并行下载,不阻塞 HTML 解析。它会等到 HTML 解析完成后、DOMContentLoaded 触发之前,按照脚本在文档中的顺序执行。DOMContentLoaded 会等待所有 defer 脚本执行完成,但不会等待这些脚本启动的定时器、网络请求等异步任务完成。

相关阅读:DOMContentLoaded 与 Load 事件的区别