先记住这个答案
普通 script:下载和执行都会阻塞 HTML 解析。async:下载不阻塞解析,但下载完立即执行,执行时阻塞解析,且多个 async 脚本的执行顺序与加载完成顺序一致,不保证文档顺序。defer:下载不阻塞解析,并在 HTML 解析完成后、DOMContentLoaded 事件前按文档顺序执行。defer 脚本会延迟 DOMContentLoaded 的触发。
- async 下载完就执行,执行时阻塞解析
- defer 在解析完成后按顺序执行
- DOMContentLoaded 会等待 defer 脚本
三种模式的处理机制
普通 script:当解析器遇到带有 src 的 script 标签(无 async/defer/module),会立即发起请求并暂停解析。下载完成后同样阻塞解析执行脚本,执行完毕后才继续解析后续 HTML。这种同步行为保证脚本能访问其之前定义的 DOM,但也让后续内容延迟呈现。
async 脚本在下载阶段并行进行,HTML 解析继续。但下载一旦完成,就会立刻中断解析并执行(除非此时解析已结束)。由于网络速度不同,先加载完的 async 脚本先执行,所以无法保证多个 async 脚本的执行顺序与出现顺序一致。执行期间仍会阻塞解析,只是这段时间取决于脚本运行时长。
defer 脚本也会异步下载,但执行被推迟到 HTML 解析完全结束后、DOMContentLoaded 事件触发之前。多个 defer 脚本会严格按照文档中出现顺序执行,即使先下载完的也等待前序脚本。defer 脚本执行完毕后 DOMContentLoaded 才会触发,因此 DOMContentLoaded 的监听器总能拿到其修改后的 DOM。
电商页面加载统计与广告脚本
页面 head 中有一个需要读取完整 DOM 的埋点脚本 analytics.js,还有两个互相独立的广告脚本 ad1.js、ad2.js,HTML body 很长且含大量图片。目标是尽早解析首屏,同时保证埋点不依赖加载状态。给这两个广告脚本加 async,埋点标签加 defer。这样广告下载不阻塞解析,完成后各自执行,即便广告先加载完,也只会延迟执行时的那一小段时间,首屏关键内容能够更快解析。defer 的 analytics.js 在解析完成后执行,确保能遍历全部 DOM 节点并绑定事件,且一定赶在 DOMContentLoaded 之前。
如果将所有脚本都做成 async,广告脚本可能先于 analytics 执行,此时 DOM 尚未完整,且 ad1 与 ad2 的执行顺序无法保证,可能引发与顺序相关的竞态条件。如果都用 defer,虽然顺序确定,但浏览器无法并行控制执行优先级,某个大脚本可能延迟 DOMContentLoaded。因此选择 async 给无依赖的独立脚本,defer 给需要 DOM 且要求顺序的脚本。
失效条件与代价
defer 脚本本身仍然会阻塞 DOMContentLoaded:下载慢的 defer 脚本会推迟事件,导致用户可能看到内容却无法触发逻辑。如果只关心脚本间顺序而不是整个 DOM,async 提供的顺序不确定性可能在多脚本间产生竞争条件。async 只适用于完全独立的脚本,否则后果是程序行为不可预测。
动态创建的 script 并插入 DOM 时,默认按 async 行为执行;若需按序执行可设置 script.async = false,但不会像 defer 那样推迟到 DOMContentLoaded,只会按插入顺序尽快执行。调试时要注意 async 脚本的执行顺序随下载速度变化,难以复现,必要时需改用 defer 或改用模块 type="module"(其隐式 defer)。
容易答错的地方
- 认为 async 和 defer 只在下载阶段有区别
- 关键区别在执行:async 下载完就立即执行,可能阻塞解析且顺序不保证;defer 要等解析完并按文档顺序执行。误以为两者相似会导致脚本顺序依赖错误。
- 以为 DOMContentLoaded 总是先于 async 执行
- 如果 async 脚本下载极快,可能在 HTML 解析完成前执行,因此其执行可能在 DOMContentLoaded 之前也可能之后。defer 则必然在 DOMContentLoaded 前执行。
面试官还会怎么问?
多个 defer 脚本中混入一个 async 脚本,执行顺序会怎样?
async 脚本下载完就执行,不排队,所以可能在 defer 脚本之间突然执行,也可能之后,因此不能假设它相对于 defer 的顺序。多个 defer 仍然按文档序,在解析后执行,async 则不一定。
脚本同时写了 async 和 defer,最后实际行为是什么?
对于普通脚本,async 优先级更高,defer 被忽略。浏览器只把该脚本当 async 处理:下载完立即执行,顺序无关。建议不要同时使用以免误解。
动态创建 script 并设置 src 后,默认是阻塞还是 async?
默认是 async 行为:下载完立刻执行,不保证顺序。若想按顺序加载,需要设置 script.async = false,此时会按插入顺序执行,但都会在下载完成后尽快执行,不等 DOMContentLoaded。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。