defer 和 async 都可以让外部脚本在下载时不阻塞 HTML 解析,但它们的执行时机不同:
defer:等 HTML 解析完成后,按照脚本在文档中的顺序执行。async:脚本下载完成后尽快执行,不保证执行顺序。对于 defer,更准确的执行条件是:
HTML 已经解析完成、脚本已经下载完成,并且已经轮到该脚本按文档顺序执行。
因此:
也就是:
defer 脚本执行时,完整的 DOM 已经构建完成,所以可以直接访问页面中的 DOM 元素。
| 写法 | 下载行为 | 执行时机 | 执行顺序 | 对 HTML 解析的影响 |
|---|---|---|---|---|
<script src="a.js"> | 遇到脚本后下载 | 下载完成后立即执行 | 按文档顺序 | 下载和执行都会暂停解析 |
<script async src="a.js"> | 与 HTML 解析并行 | 下载完成后尽快执行 | 不保证 | 下载不阻塞,执行时可能暂停解析 |
<script defer src="a.js"> | 与 HTML 解析并行 | HTML 解析完成后、DOMContentLoaded 之前 | 按文档顺序 | 不会暂停 HTML 解析 |
可以简单理解为:
async:谁先下载完,谁先执行。defer:先解析完 HTML,再排队执行。即使 b.js、c.js 比 a.js 更早下载完成,执行顺序仍然是:
因此,当 b.js 依赖 a.js 时,可以使用 defer 保证依赖顺序。async 不适合这种场景。
正常情况下不会。
浏览器必须先执行完 defer 脚本,才会触发 DOMContentLoaded;而 window.load 又发生在 DOMContentLoaded 之后。因此,window.load 不可能在 defer 脚本的同步代码执行完之前触发。
但需要区分:
defer 脚本本身执行完毕。defer 脚本启动的异步任务执行完毕。这里的 defer.js 已经执行完毕,只是它创建的定时器回调还没有执行。浏览器不会为了这个异步回调而推迟 DOMContentLoaded 或 window.load。
适合普通业务脚本,尤其是:
适合与页面其他代码没有依赖关系的独立脚本,例如:
defer 主要用于带有 src 的外部经典脚本,下面的 defer 不会产生推迟效果:
模块脚本默认不会阻塞 HTML 解析,并且会在文档解析完成后执行:
通常不需要再为模块脚本添加 defer。如果添加 async,它会在依赖模块下载完成后尽快执行,不再等待文档解析完成。
defer脚本会与 HTML 并行下载,不阻塞 HTML 解析。它会等到 HTML 解析完成后、DOMContentLoaded触发之前,按照脚本在文档中的顺序执行。DOMContentLoaded会等待所有defer脚本执行完成,但不会等待这些脚本启动的定时器、网络请求等异步任务完成。