16、defer 和 async 区别
目录
一、先建立认知框架
二、三种加载方式的完整对比
时序图
核心属性对比
三、逐项深度解析
执行时机的本质差异
与 DOMContentLoaded 的关系
顺序保证的工程意义
对 DOM 的访问能力
四、动态创建 script 标签
五、module script 的加载行为
六、工程选型原则
七、两个面试回答模板
🏆 高分模板(展现系统性 + 工程深度)
📝 简答模板(30 秒快速作答版)
八、面试官常见追问
这道题是浏览器渲染原理的高频细节题,能答好的关键在于:不只说"defer 等解析完执行,async 下载完就执行",而是要讲清楚执行时机的本质差异、顺序保证、依赖关系的影响、以及工程中如何正确选型,这才是让面试官眼前一亮的回答方式。
一、先建立认知框架
"defer 和 async 都是为了解决普通 script 阻塞 HTML 解析的问题,两者都会并行下载 JS,区别在于执行时机不同:defer 等 HTML 解析完再执行,async 下载完立即执行。一个让 JS 靠后执行,一个让 JS 尽快执行。"
二、三种加载方式的完整对比
时序图
普通 script(阻塞): HTML: ===解析==[暂停]=====[恢复]===解析完成 JS: [下载][执行] defer(延迟执行): HTML: ===解析========================解析完成 JS: [----并行下载----] [执行] ↑ HTML 解析完才执行 async(异步执行): HTML: ===解析==========[暂停]==[恢复]===解析完成 JS: [----并行下载----][执行] ↑ 下载完立即执行,打断 HTML 解析核心属性对比
| 维度 | 普通 script | defer | async |
|---|---|---|---|
| 下载时机 | 立即,阻塞解析 | 并行下载 | 并行下载 |
| 执行时机 | 下载完立即执行 | HTML 解析完后执行 | 下载完立即执行 |
| 是否阻塞解析 | ✅ 下载和执行都阻塞 | ❌ 不阻塞 | ⚠️ 执行时阻塞 |
| 执行顺序 | 按 HTML 顺序 | 按 HTML 顺序 | 不保证顺序 |
| DOMContentLoaded | 触发前执行 | 触发前执行 | 不确定 |
三、逐项深度解析
执行时机的本质差异
defer 的执行时机:
HTML 解析完成 ↓ DOMContentLoaded 事件触发之前 ↓ 按照 script 在 HTML 中出现的顺序执行 defer 脚本 ↓ DOMContentLoaded 事件触发<script src="a.js" defer></script> <script src="b.js" defer></script> <script src="c.js" defer></script> <!-- 无论谁先下载完,执行顺序一定是 a → b → c -->async 的执行时机:
哪个 JS 文件先下载完,哪个先执行 ↓ 执行时中断当前 HTML 解析 ↓ 执行完毕,恢复解析<script src="a.js" async></script> <script src="b.js" async></script> <script src="c.js" async></script> <!-- 谁先下载完谁先执行,顺序完全不确定 --> <!-- 可能是 c → a → b,也可能是 b → c → a -->与 DOMContentLoaded 的关系
"这是区分两者的一个关键细节,面试说出来是加分点。"
defer 与 DOMContentLoaded:
规范明确规定: defer 脚本必须在 DOMContentLoaded 事件触发之前执行完毕 所以: document.addEventListener('DOMContentLoaded', () => { // 此时所有 defer 脚本都已执行完 })async 与 DOMContentLoaded:
async 脚本与 DOMContentLoaded 没有顺序保证: 情况一:JS 文件很小,解析完成前就下载完并执行了 → async 先执行,DOMContentLoaded 后触发 情况二:JS 文件很大,解析完成后才下载完 → DOMContentLoaded 先触发,async 后执行 完全取决于网络速度和文件大小,不可预测顺序保证的工程意义
为什么顺序重要:
// a.js:定义了工具函数 function formatDate(date) { ... } // b.js:依赖 a.js 的函数 formatDate(new Date()); // 如果 b 先执行,formatDate 未定义,报错<!-- defer 保证顺序,这样写是安全的 --> <script src="a.js" defer></script> <script src="b.js" defer></script> <!-- async 不保证顺序,b 可能先于 a 执行,导致报错 --> <script src="a.js" async></script> <script src="b.js" async></script> <!-- 危险! -->对 DOM 的访问能力
defer 执行时 DOM 已构建完成:
// a.js(defer 加载) document.querySelector('.header').style.color = 'red'; // ✅ 安全,执行时 HTML 已解析完,DOM 完整async 执行时 DOM 不一定完整:
// a.js(async 加载) document.querySelector('.header').style.color = 'red'; // ⚠️ 危险!如果在 HTML 解析完成前就执行了 // .header 元素可能还没解析到,querySelector 返回 null四、动态创建 script 标签
"面试中经常追问的边界场景。"
const script = document.createElement('script'); script.src = 'app.js'; document.head.appendChild(script);动态创建的 script 标签默认行为是 async:
// 等价于 <script src="app.js" async></script> // 如果需要 defer 行为(保证顺序),需要显式设置: script.async = false;这是很多脚本加载器(Script Loader)和模块打包工具的底层实现细节。
五、module script 的加载行为
"ES Module 默认就是 defer 行为,也是面试加分点。"
<!-- type="module" 默认等同于 defer --> <script type="module" src="app.js"></script> <!-- 等价于 --> <script src="app.js" defer></script><!-- module 也可以加 async,变成 async 行为 --> <script type="module" src="app.js" async></script>module 的特点:
① 默认 defer 行为 ② 严格模式('use strict') ③ 模块作用域(变量不污染全局) ④ 同一模块只加载一次(即使多次 import) ⑤ 支持 import/export 语法六、工程选型原则
选 defer 的场景: ✅ 有依赖关系的脚本(需要按顺序执行) ✅ 需要访问完整 DOM 的脚本 ✅ 大多数业务逻辑脚本 ✅ 框架初始化脚本(React、Vue 入口文件) 选 async 的场景: ✅ 完全独立、无依赖的脚本 ✅ 不操作 DOM 或不依赖 DOM 完整性 ✅ 第三方统计脚本(Google Analytics、埋点) ✅ 广告脚本 ✅ 不影响主要功能的辅助脚本 不用属性的场景: ✅ 必须立即执行的关键初始化代码(如检测浏览器特性) ✅ 内联脚本(无法加 async/defer,内联脚本直接执行)七、两个面试回答模板
🏆 高分模板(展现系统性 + 工程深度)
"defer 和 async 都是为了解决普通 script 阻塞 HTML 解析的问题,两者都会让 JS 文件并行下载,不阻塞 HTML 解析,区别在于执行时机和顺序保证上。
先说 defer。defer 的脚本会在 HTML 解析完成之后、DOMContentLoaded 事件触发之前按顺序执行。注意是按照 script 在 HTML 中出现的顺序,无论哪个文件先下载完,都等所有 defer 脚本准备好后按序执行。所以多个 defer 脚本之间有明确的顺序保证,可以有依赖关系。另外 defer 执行时 DOM 已经构建完成,可以安全访问任意 DOM 节点。
再说 async。async 的脚本是哪个先下载完哪个先执行,不保证顺序,执行时机是下载完立即执行,会打断当前的 HTML 解析。async 和 DOMContentLoaded 也没有顺序保证,如果文件很小解析前就下载完了就先执行,如果文件大解析完才下载完就在 DOMContentLoaded 之后执行,完全取决于网络。
两者最关键的差异可以用一句话总结:defer 让 JS 尽量靠后执行,保证顺序,保证 DOM 完整;async 让 JS 尽量快执行,不保证顺序,不保证 DOM 状态。
工程选型上,有依赖关系的业务脚本、需要操作 DOM 的脚本都应该用 defer,React、Vue 这类框架的入口文件用 defer 很合适;完全独立的第三方脚本比如埋点、广告,用 async 更合适,早点执行不影响其他。
有两个细节值得一提:第一,动态创建 script 标签默认是 async 行为,如果需要保证顺序要显式设置
script.async = false;第二,ES Module 的type=module默认就是 defer 行为,这是规范层面的设计,因为模块通常有依赖关系,需要顺序保证。"
📝 简答模板(30 秒快速作答版)
"defer 和 async 都能让 JS 并行下载,不阻塞 HTML 解析,区别在执行时机。
defer:HTML 解析完成后按顺序执行,在 DOMContentLoaded 之前。多个 defer 脚本严格按出现顺序执行,可以有依赖关系,执行时 DOM 已完整。
async:下载完立即执行,执行时会打断 HTML 解析。多个 async 脚本谁先下载完谁先执行,顺序不保证,与 DOMContentLoaded 没有顺序关系。
选型原则:有依赖、需要访问 DOM 的业务脚本用 defer;完全独立的第三方脚本(埋点、广告)用 async。
补充一个细节:ES Module 的
type=module默认是 defer 行为;动态创建的 script 标签默认是 async 行为,想保证顺序要设置script.async = false。"
八、面试官常见追问
| 追问 | 答题方向 |
|---|---|
| "defer 和把 script 放 body 底部有什么区别?" | defer 可以放 head,下载和 HTML 解析并行;放底部要等 HTML 解析完才开始下载,defer 性能更好 |
| "async 的执行会阻塞渲染吗?" | 会,执行时会打断 HTML 解析,从而延迟渲染 |
| "多个 async 脚本能保证顺序吗?" | 不能,谁先下载完谁先执行,有依赖关系的脚本不能用 async |
| "defer 一定在 DOMContentLoaded 前执行吗?" | 是的,规范明确规定 defer 脚本在 DOMContentLoaded 触发前执行完毕 |
| "动态插入的 script 是什么行为?" | 默认 async,可以设置script.async = false改为 defer 行为 |
| "type=module 和 defer 有什么区别?" | 行为类似,module 额外有严格模式、模块作用域、只加载一次、支持 import/export |
| "内联 script 可以加 defer 或 async 吗?" | 不行,defer 和 async 对内联脚本无效,内联脚本总是立即执行 |
