async脚本下载不阻塞dom解析,但执行时会立即中断解析;defer则确保执行在dom解析完成后、domcontentloaded前,且按书写顺序执行。

async 脚本下载时不阻塞 DOM 解析,但执行时会中断它
很多人以为加了 async 就彻底“不阻塞”,其实只对下载阶段成立。浏览器遇到 <script async src="a.js"></script> 会立即发起下载,并继续解析后续 HTML;但一旦脚本下载完成,JS 引擎会**立刻暂停 HTML 解析器**,同步执行该脚本——此时 DOM 构建被中断,可能造成重排、样式丢失或元素查不到。
- 常见错误现象:
document.getElementById('main')在async脚本里返回null,因为#main还没被解析到 - 执行时机完全不可控:网络快的脚本哪怕体积小,也可能在
开头就执行,远早于 DOM 就绪 -
async不保证执行顺序,多个async脚本之间不能有依赖(比如先加载lodash.js再用_.debounce) - Chrome DevTools 的 **Timeline / Performance 面板**里能看到明显的“Script Evaluation”打断 HTML 解析线程
defer 才真正避开 DOM 构建阶段的执行干扰
defer 的关键优势不是“更快”,而是**把执行时机明确推迟到 DOM 解析完成之后、DOMContentLoaded 触发之前**。它下载并行,执行排队,且不打断 HTML 解析流程——这意味着首屏元素能完整进入 DOM 树、参与样式计算和布局,再等脚本来操作它们。
- 适合场景:初始化 UI 组件、绑定按钮事件、读取
document.body或表单元素 - 多个
defer脚本严格按<script></script>标签在 HTML 中的书写顺序执行 - 注意:
defer仍会阻塞DOMContentLoaded事件触发——只要脚本没执行完,这个事件就不发 - IE9 及更旧版本不支持
defer,会降级为同步加载;若需兼容,得用动态插入 +document.readyState判断
为什么 document.getElementById 在 async 脚本里总报 null
这不是代码写错了,是 async 的行为设计使然。它根本不承诺 DOM 就绪,只承诺“下载不卡 HTML”。哪怕脚本只有 1KB、CDN 响应 20ms,只要它比 HTML 解析快,就会在 <div id="app"> 这行还没被 parser 处理前就执行。
<ul>
<li>修复方式不是“等一会儿”,而是主动守界:<code>if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init) }
const el = document.getElementById('btn') || document.querySelector('body').querySelector('#btn')(不推荐,治标不治本)defer 脚本里,或用 type="module"(默认 defer 语义 + 顶层 await 支持)async 无效,浏览器直接忽略——async 只对带 src 的外部脚本起作用async 和 defer 混用会破坏调度预期
如果一个页面同时存在 <script async src="analytics.js"></script> 和 <script defer src="ui.js"></script>,浏览器不会统一调度它们的执行时间点。前者可能在 解析一半时就执行,后者则等到整个 HTML 解析完才跑——这种混合容易导致全局变量覆盖、状态错乱或竞态条件。
- 典型问题:
analytics.js里写了window.tracker = {...},ui.js里又重写了window.tracker.init(),但async版本执行晚于defer版本,结果init()调用时报undefined - 解决方案:按职责分离——统计/埋点类用
async,业务逻辑/ DOM 操作类统一用defer,避免交叉依赖 - 动态创建的
script元素(document.createElement('script'))默认行为接近async,但可通过插入位置和onload控制,适合高风险脚本隔离











