script 标签的 defer 和 async 有什么区别?
简化版
defer 和 async 都可以让脚本下载不阻塞 HTML 解析。区别是:defer 会等 HTML 解析完成后按顺序执行,适合依赖 DOM 或有顺序依赖的脚本;async 下载完就执行,执行时可能打断解析,适合独立脚本,如统计 SDK。
详细版
普通脚本会阻塞 HTML 解析:浏览器遇到脚本后下载并执行,执行完再继续解析。
defer:
- 下载不阻塞解析。
- 等 DOM 解析完成后执行。
- 多个 defer 脚本按文档顺序执行。
- 适合业务主脚本。
async:
- 下载不阻塞解析。
- 下载完成后立即执行。
- 执行顺序不确定。
- 适合无依赖第三方脚本。
完整版教学
一、普通 script 为什么阻塞
同步脚本可能修改 DOM,也可能调用 document.write。浏览器为了保证执行结果正确,遇到普通脚本时会暂停 HTML 解析,等待脚本下载和执行完成。
如果脚本很大或网络慢,页面就可能长时间白屏。
二、defer 的特点
defer 表示延迟执行。浏览器会并行下载脚本,但不会立刻执行,而是等 HTML 解析完成后再按顺序执行。
它适合大多数业务脚本,因为业务脚本通常依赖 DOM 结构,并且多个脚本之间可能有顺序关系。
在现代工程中,构建工具输出的入口脚本经常使用类似 defer 的加载策略。
三、async 的特点
async 表示异步脚本。浏览器并行下载脚本,下载完成后马上执行。它不保证多个 async 脚本的执行顺序。
因此它适合独立脚本,比如统计、广告、监控 SDK。它们不应该依赖页面主业务,也不应该被主业务依赖。
四、面试追问与工程落地
面试官可能问:“defer 脚本和 DOMContentLoaded 的关系?”
通常 DOMContentLoaded 会等待 defer 脚本执行完成后再触发,而 async 脚本不保证在 DOMContentLoaded 前还是后执行。
工程中选择很简单:业务主脚本优先 defer,第三方独立脚本可 async。不要让 async 脚本之间互相依赖顺序。
五、下载与执行时间线
以两个脚本 A、B 为例,普通经典脚本在解析器遇到时获取并执行;async 谁先下载完谁先执行;defer 可并行下载,但等文档解析完成后按文档顺序 A→B 执行,并且 DOMContentLoaded 要等待它们执行完。
普通:HTML ──停──[下载 A][执行 A]──继续解析
async:HTML ─────解析─────┐
[下载 A]→[执行 A,暂停解析]
defer:HTML ─────解析完成────────DOMContentLoaded
[下载 A] ──┐ [按序执行 A、B] ─┘
[下载 B] ──┘
| 类型 | 获取是否阻塞解析 | 执行时机 | 顺序保证 |
|---|---|---|---|
| classic 普通外链 | 是 | 获取后立即 | 文档顺序 |
classic async | 否 | 获取完成即执行 | 无 |
classic defer | 否 | 解析后、DOMContentLoaded 前 | 文档顺序 |
type="module" | 否 | 默认类似 defer | 依赖图顺序 |
若 A 下载 300ms、B 下载 100ms,两个 async 通常 B 先执行;两个 defer 仍保持 A→B,只是可能等待较慢的 A。这正是 defer 适合有依赖业务入口、async 适合相互独立脚本的原因。
六、module、动态脚本与失败边界
defer 对没有 src 的内联经典脚本没有效果;module script 默认延迟执行,所以再写 defer 没有额外意义。给 module 加 async 会让其及依赖准备好后尽快执行,不再等待文档解析完成的默认时机。
同时写 async defer 时,现代浏览器对支持 async 的经典脚本按 async 处理,历史上 defer 可作为旧浏览器回退,但新代码不应依赖这种技巧。通过 JS 动态创建的脚本默认具有异步执行特征,若必须保持顺序,需要显式管理 async、插入次序或用 module import 依赖图。
第三方脚本即使 async,执行时仍占主线程。一个统计 SDK 下载不阻塞解析,但若执行 120ms,仍会形成长任务并推迟输入与绘制;还应配置超时、错误隔离、CSP/SRI(适用时)和加载失败降级。
记忆钩子:async 管“谁先到谁先跑”,defer 管“解析后按队列跑”;两者只让下载并行,不让 JavaScript 执行脱离主线程。
选型检查表
选择属性前依次确认:脚本是否依赖 DOM、是否依赖前序脚本、是否必须在 DOMContentLoaded 前完成、失败后主业务能否继续。业务依赖链优先由 module 图或 defer 保序,监控/广告等独立脚本才考虑 async,并为失败和超时准备降级。
如果第三方脚本需要调用页面初始化函数,应由明确的 load/Promise 协议协调,而不是假设它会在某个毫秒数内下载完成。网络缓存命中会改变到达顺序,慢网和 CDN 故障更会暴露偶然时序。
七、常见误区与追问
- 误区:async 脚本的执行也不会阻塞 HTML 解析。 下载并行,但脚本准备好后执行会占主线程并暂停解析。
- 误区:defer 脚本会在
load事件之后执行。 它们在DOMContentLoaded前完成,load 还要等待图片等资源。 - 误区:module script 必须加 defer 才不阻塞解析。 module 默认就是延迟执行语义,defer 属性对它没有额外作用。
- 追问:两个 async 脚本为什么不能互相依赖? 执行顺序由下载完成时间决定,网络变化会让顺序不稳定。
- 追问:defer 下载失败会怎样影响 DOMContentLoaded? 失败脚本不会正常执行,浏览器完成其加载处理后才推进相应事件,业务应监听错误并降级。
- 追问:动态插入脚本如何保证顺序? 显式设置
async=false并控制插入,或使用 Promise/module 依赖关系,不靠偶然下载顺序。 - 追问:第三方脚本用了 async 就没有性能风险吗? 不是,执行、DOM 操作和后续任务仍可能造成长任务与布局开销。
八、加强记忆
脚本加载分“获取”和“执行”两件事:普通脚本两者都挡解析,async 只并行获取、到达就执行且不保序,defer 并行获取、解析后按序执行并先于 DOMContentLoaded。module 默认 defer,动态脚本与第三方脚本还有独立时序;任何脚本真正执行时仍会占主线程,因此还要控制体积和运行成本。