defer脚本总在DOMContentLoaded前执行,因其设计为下载不阻塞、执行等DOM就绪;浏览器按HTML中顺序在DOM树构建完但事件触发前执行所有defer脚本。

defer 和 async 都能避免脚本阻塞 HTML 解析,但执行时机和顺序保障完全不同:defer 保证顺序且等 DOM 解析完再执行;async 不保序、加载完就执行,可能打断解析。
defer 脚本为什么总在 DOMContentLoaded 前执行
因为 defer 的设计逻辑就是“下载不阻塞,执行等 DOM 就绪”。浏览器把所有 defer 脚本放进一个队列,等整个 HTML 字符串解析完成(即 DOM 树构建完毕)、但还没触发 DOMContentLoaded 事件时,按它们在 HTML 中的出现顺序依次执行。
这意味着:
-
document.getElementById一定能拿到页面上任意元素,哪怕它写在<head>里 - 多个
defer脚本之间有明确依赖时(比如先加载utils.js,再加载main.js),不会因网络快慢错乱执行顺序 - 如果某个
defer脚本里写了document.addEventListener('DOMContentLoaded', ...),这个监听器实际不会被触发——因为事件还没发,脚本就已经在事件前执行完了
async 脚本加载完就执行,可能破坏 DOM 访问
async 脚本一旦下载完成,浏览器会立刻暂停 HTML 解析,执行它。此时 DOM 树大概率没建完,document.body 可能是 null,querySelector 找不到后面才出现的元素。
典型问题场景包括:
- 在
async脚本里直接操作document.getElementById('app'),而<div id="app">在 HTML 后半部分 —— 报错Cannot read property 'appendChild' of null - 两个
async脚本,a.js依赖b.js提供的全局函数,但a.js先加载完、先执行,报ReferenceError: initApp is not defined - 脚本执行期间 HTML 解析被中断,用户看到“页面卡住半截”的现象(尤其在低端设备上更明显)
哪些脚本该用 defer,哪些必须用 async
判断依据不是“要不要快”,而是“能不能独立运行”和“是否需要完整 DOM”。
应该用 defer 的情况:
- 框架入口文件,如
<script defer src="vue.runtime.esm-bundler.js"></script> - 业务主逻辑,比如初始化表单校验、绑定事件,依赖
document.querySelectorAll('.submit-btn') - 任何包含
document.write(虽然已不推荐)或显式操作 DOM 的脚本
应该用 async 的情况:
- 统计类脚本,如
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXX"></script>—— 它只发请求,不读 DOM - 广告 SDK,比如
<script async src="//ad.example.com/sdk.js"></script>—— 自行管理生命周期,不依赖页面结构 - 纯工具函数库(无副作用、不操作 DOM、不依赖其他脚本),且你确认它不会被其他脚本同步调用
IE9–10 兼容性下怎么安全 fallback
defer 在 IE9+ 支持,async 要 IE10+。如果你的项目还要支持 IE9,不能直接删掉属性——浏览器会把它当普通脚本同步执行,白屏风险回归。
稳妥做法是:
- 把关键脚本移到
<body>底部,靠近</body>,这是最古老也最可靠的非阻塞方案 - 用动态插入脚本的方式模拟
defer行为:document.addEventListener('DOMContentLoaded', () => { loadScript('main.js'); }); - 避免在
<head>里混用async和defer—— IE9 会忽略两者,但现代浏览器行为又不一致,调试起来非常绕
真正容易被忽略的点是:async 和 defer 都只对 src 外链脚本生效;内联脚本加了这两个属性会被浏览器直接忽略,照样同步执行。

















