defer和async是控制JavaScript脚本加载的两种方式。
传统脚本会阻塞HTML解析。
defer在后台下载脚本并在DOM解析完成后按顺序执行,适合依赖DOM或有依赖关系的脚本。
async下载完成后立即执行,但不保证顺序,适合独立脚本。
defer保证执行顺序且不阻塞解析,应在大多数情况下优先使用,而async适用于无依赖的第三方脚本。
正确使用这些属性可优化页面加载性能。
defer 和 async 是 <script> 标签的两个属性,用于控制 JavaScript 脚本的加载时机 和执行顺序。
它们解决了传统脚本加载会阻塞 HTML 解析的问题。
1. 传统脚本(无属性)
html
<script src="script.js"></script>
行为:
-
浏览器解析 HTML,遇到
<script>标签 -
暂停 HTML 解析
-
下载脚本(阻塞)
-
执行脚本(阻塞)
-
恢复 HTML 解析
问题:如果脚本很大、网络慢,用户会看到白屏,页面长时间无响应。
2. defer(延迟执行)
html
<script src="script.js" defer></script>
行为:
-
浏览器遇到
<script defer>,不阻塞 HTML 解析,在后台并行下载脚本 -
HTML 解析完成后(
DOMContentLoaded事件触发前),按照脚本在文档中的顺序依次执行 -
执行完所有
defer脚本后,触发DOMContentLoaded事件
特点:
-
✅ 不阻塞 HTML 解析
-
✅ 保证执行顺序(按出现顺序)
-
✅ 在
DOMContentLoaded之前执行 -
✅ 适合需要操作 DOM、且脚本之间有依赖关系的场景
3. async(异步执行)
html
<script src="script.js" async></script>
行为:
-
浏览器遇到
<script async>,不阻塞 HTML 解析,在后台并行下载脚本 -
脚本下载完成后立即执行(此时可能 HTML 还未解析完)
-
执行时阻塞 HTML 解析(如果 HTML 还在解析中)
特点:
-
✅ 不阻塞 HTML 解析(下载阶段)
-
⚠️ 不保证执行顺序(谁先下载完谁先执行)
-
⚠️ 执行时机不确定,可能在
DOMContentLoaded之前或之后 -
✅ 适合独立的、无依赖关系的脚本(如埋点统计、独立组件)
4. 对比总结
| 特性 | 无属性 | defer |
async |
|---|---|---|---|
| 下载时机 | 遇到时立即下载 | 遇到时后台下载 | 遇到时后台下载 |
| 是否阻塞 HTML 解析 | ✅ 阻塞 | ❌ 不阻塞(下载阶段) ✅ 阻塞(执行阶段) | ❌ 不阻塞(下载阶段) ✅ 阻塞(执行阶段) |
| 执行时机 | 下载后立即执行 | HTML 解析完成后 | 下载完成后立即执行 |
| 执行顺序 | 按出现顺序 | ✅ 按出现顺序 | ❌ 不保证顺序 |
| DOMContentLoaded | 等待脚本执行完成 | 等待所有 defer 脚本执行完成 |
与 async 无关,可能早于或晚于 |
5. 可视化执行流程
html
无属性:
HTML 解析: [=======] [暂停] [=======]
脚本下载: [下载]
脚本执行: [执行]
defer:
HTML 解析: [========================]
脚本下载: [下载1] [下载2] [下载3]
脚本执行: [执行1][执行2][执行3] → DOMContentLoaded
async:
HTML 解析: [==========] [暂停] [=======] [暂停] [===]
脚本下载: [下载1] [下载2] [下载3]
脚本执行: [执行1] [执行2] [执行3]
6. 实际使用场景
场景 1:使用 defer
-
需要操作 DOM 的脚本
-
脚本之间有依赖关系(如 jQuery 在前,其他插件在后)
-
多个脚本需要按特定顺序执行
html
<script src="jquery.js" defer></script>
<script src="plugin.js" defer></script> <!-- 依赖 jQuery,必须在其后执行 -->
<script src="app.js" defer></script> <!-- 依赖两者 -->
场景 2:使用 async
-
完全独立的脚本,不依赖其他脚本,也不依赖 DOM
-
第三方统计代码(如 Google Analytics)
-
广告脚本、社交插件
html
<script src="https://www.googletagmanager.com/gtag.js" async></script>
<script src="ad-provider.js" async></script> <!-- 两个脚本互不依赖 -->
场景 3:内联脚本
defer 和 async 只对外部脚本(有 src 属性)生效,对内联脚本无效:
html
<!-- 无效,会被忽略 -->
<script defer>
console.log('内联脚本不能 defer');
</script>
7. 常见误区
误区 1:defer 不会阻塞 DOMContentLoaded
✅ 正确。但注意:defer 脚本会在 DOMContentLoaded 之前 执行,所以 DOMContentLoaded 会等待所有 defer 脚本执行完毕。
误区 2:async 一定在 DOMContentLoaded 之前执行
❌ 错误。如果脚本下载很快,可能在 HTML 解析完成前就执行;如果下载慢,可能在 DOMContentLoaded 之后执行。
误区 3:defer 和 async 都能提升性能
⚠️ 不一定。如果脚本有依赖关系,用错 async 可能导致执行顺序错乱、报错。选对属性才能提升性能。
8. 与 DOMContentLoaded 和 load 的关系
javascript
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 解析完成,所有 defer 脚本已执行');
});
window.addEventListener('load', () => {
console.log('所有资源(图片、样式、async 脚本等)加载完成');
});
| 脚本类型 | 与 DOMContentLoaded 的关系 |
|---|---|
| 无属性 | 阻塞 DOMContentLoaded |
defer |
DOMContentLoaded 等待所有 defer 执行完成 |
async |
与 DOMContentLoaded 无关,可能在前后都可能 |
9. 最佳实践建议
-
优先使用
defer:大多数业务脚本都适合defer,既能并行下载,又能保证执行顺序 -
独立脚本用
async:埋点、广告等无依赖的第三方脚本 -
关键脚本可以放在
<head>并用defer:避免阻塞首屏渲染 -
避免在
<head>中使用无属性脚本:会严重阻塞页面渲染 -
尽量将脚本放在
<body>底部 :即使不加属性,也能减少阻塞影响(但不如defer优雅)
10. 快速决策表
| 你的需求 | 推荐方案 |
|---|---|
| 脚本需要操作 DOM | defer |
| 脚本之间有依赖关系 | defer |
| 脚本完全独立,不依赖任何东西 | async |
| 需要在 HTML 解析过程中立即执行 | 无属性,放在需要的位置 |
| 不确定选哪个 | 默认选 defer |