script 标签的 async 和 defer 属性有什么区别?
script 标签的 async 和 defer 属性都用于异步加载外部 JavaScript 文件,目的是为了避免脚本加载和执行阻塞 HTML 文档的解析(即避免“阻塞渲染”)。
它们的主要区别在于 执行的时机 和 执行的顺序。
为了更直观地理解,我们可以把过程分为三个部分:
- HTML 解析 (Parsing)
- 脚本下载 (Fetching)
- 脚本执行 (Execution)
1. 图解对比
假设 HTML 解析过程中遇到了一个 <script src="..."> 标签:
普通
<script>(无属性):
HTML 解析停止 -> 下载脚本 -> 执行脚本 -> HTML 解析继续
(完全阻塞)<script async>:
HTML 解析继续 + 下载脚本 (并行) -> (下载完立即) HTML 解析暂停 -> 执行脚本 -> HTML 解析继续
(下载不阻塞,执行阻塞)<script defer>:
HTML 解析继续 + 下载脚本 (并行) -> HTML 解析完成 -> 执行脚本
(下载不阻塞,执行也不阻塞)
2. 详细区别
A. 执行时机 (When)
- async (Asynchronous - 异步):
- 脚本一旦下载完成,立即执行。
- 这意味着它会打断 HTML 的解析。如果脚本很小下载很快,它可能很快就打断解析;如果脚本很大,它可能在解析完很久之后才执行。
- defer (Deferred - 推迟):
- 脚本下载完成后不立即执行。
- 它会等到整个 HTML 文档解析完成(DOM 构建完毕),但在
DOMContentLoaded事件触发之前执行。
B. 执行顺序 (Order)
- async:
- 无序。谁先下载完,谁先执行。
- 如果你有
script A和script B,且 B 依赖于 A,使用async可能会导致报错,因为 B 可能比 A 先下载完并执行。
- defer:
- 有序。严格按照它们在 HTML 中出现的顺序执行。
- 即使
script B比script A先下载完,浏览器也会等A执行完再执行B。
3. 总结对比表
| 特性 | <script> (默认) |
<script async> |
<script defer> |
|---|---|---|---|
| HTML 解析 | 被阻塞 (下载和执行时) | 仅在执行时被阻塞 | 不阻塞 (直到解析完成) |
| 脚本下载 | 阻塞 HTML 解析 | 异步 (并行) | 异步 (并行) |
| 执行时机 | 下载完成后立即执行 | 下载完成后立即执行 | HTML 解析完成后执行 |
| 执行顺序 | 按文档顺序 | 网络下载速度决定 (无序) | 按文档顺序 |
| 适用场景 | 必须立即执行且依赖 DOM 的关键脚本 | 统计代码、广告、不依赖 DOM 或其他脚本的独立文件 | 依赖 DOM 元素、依赖其他脚本、应用逻辑代码 |
4. 最佳实践建议
使用
defer的情况(推荐默认选项):- 如果你的脚本依赖于 DOM 元素(例如需要
document.querySelector获取元素)。 - 如果你的脚本之间有依赖关系(例如 jQuery 插件依赖 jQuery 库)。
- 通常建议将大部分应用逻辑脚本设置为
defer,放在<head>中,这样既能尽早开始下载,又不会阻塞页面渲染。
- 如果你的脚本依赖于 DOM 元素(例如需要
使用
async的情况:- 脚本完全独立,不依赖其他脚本,也不依赖 DOM 结构。
- 例如:Google Analytics(谷歌统计)、广告脚本、性能监控脚本。这些脚本什么时候加载完就什么时候跑,不影响页面主要功能。
都不用的情况:
- 如果你需要脚本在页面渲染的最早期就执行(例如加载一些 CSS Polyfill 或者防止页面闪烁的脚本),且必须阻塞后续解析以确保正确性,则直接使用普通
<script>。
- 如果你需要脚本在页面渲染的最早期就执行(例如加载一些 CSS Polyfill 或者防止页面闪烁的脚本),且必须阻塞后续解析以确保正确性,则直接使用普通