async脚本下载后立即执行,可能dom未就绪;defer脚本按序执行且确保dom就绪后运行。async适用于完全独立脚本,defer适合有依赖的库与业务代码组合。

async 和 defer 都能避免脚本阻塞 HTML 解析,但执行时机和顺序保障完全不同——选错一个,就可能报 document.getElementById is not a function 或者库没加载完业务代码就执行了。
async 脚本执行时机不确定,可能在 DOM 构建中途就运行
浏览器遇到 <script src="a.js" async></script> 时,会立即发起下载,但一旦下载完成,不管此时 HTML 解析到哪一步,都会立刻中断解析、执行脚本。
常见错误现象:
- 脚本里调用
document.querySelector('#app')返回null,因为 DOM 还没解析到那个元素 - 多个
async脚本中,utils.js比main.js下载慢,结果main.js先执行,报undefined is not a function
适用场景只有一类:完全独立、不操作 DOM、不依赖其他 JS 的脚本,比如:
-
analytics.js(埋点统计) -
ads.js(第三方广告) -
error-tracking.js(错误上报)
defer 脚本严格按顺序执行,且一定等 DOM 就绪后才跑
defer 脚本也会异步下载,但执行被“锁死”在两个关键节点之间:HTML 解析完成之后、DOMContentLoaded 事件触发之前。
这意味着:
- 所有
defer脚本都能安全访问完整 DOM,document.body、document.head全部可用 - 即使
vue.js下载慢、app.js下载快,也一定先执行vue.js(按 HTML 中声明顺序) - 如果页面里有内联脚本(无
src),它不会受defer影响,仍按位置同步执行
典型用法是「库 + 业务」组合:
<script src="lodash.js" defer></script><br><script src="init.js" defer></script>
注意:defer 对没有 src 的内联脚本无效,也不会延迟执行。
async 和 defer 不能共存,且都不支持内联脚本延迟执行
浏览器会忽略同时带 async 和 defer 的脚本,按规范只认 async 行为;而内联脚本(如 <script>console.log('hi')</script>)加了这两个属性也没用——它们只对带 src 的外部脚本生效。
容易踩的坑:
- 误以为
<script defer>init();</script>会延迟执行,其实它立刻运行 - 在 Webpack/Vite 项目里手动加
defer,但构建产物已用type="module",此时defer实际被忽略(模块脚本默认具 defer 语义) - 服务端渲染(SSR)中,把本该 defer 的初始化逻辑写在
同步脚本里,导致首屏白屏时间变长
动态创建 script 标签是更灵活的替代方案
当需要控制加载时机(比如用户点击后再加载)、或兼容老浏览器时,document.createElement('script') 更可靠。
关键细节:
- 默认行为类似
async:插入即下载,下载完立即执行 - 设
script.async = false可模拟defer效果(需确保在document.readyState === 'loading'时插入) - 可监听
load事件做链式加载:script.onload = () => loadNext()
真正难处理的不是“怎么加属性”,而是搞清脚本之间的依赖关系和 DOM 访问边界——async 不保证 DOM 存在,defer 不保证资源(如图片、iframe)已加载完成。这两点漏掉一个,调试时就会卡在“明明写了 document.getElementById,为什么还是 null”。











