defer必须用于操作dom或有脚本依赖的场景,因其在dom解析完成、domcontentloaded前按序执行;async仅适用于完全独立、不操作dom的脚本,下载完立即执行且顺序不可控。

别看“async”字面意思,它不保证 DOM 就绪,也不保执行顺序——选错就直接报 document.getElementById 返回 null。
什么时候必须用 defer 而不是 async
脚本要操作 DOM 或依赖其他脚本执行结果时,defer 是唯一安全选项。
- 典型场景:初始化 Vue/React 的
#app挂载点、绑定按钮事件、调用document.querySelector、依赖前一个脚本导出的全局变量(如先加载lodash.js,再用_.debounce) -
defer脚本一定在 HTML 解析完成、DOMContentLoaded触发前执行,DOM 树完整可用 - 多个
defer脚本严格按 HTML 中出现顺序执行,适合构建依赖链 - 注意:
defer只对带src的外部脚本生效;<script defer>console.log(1)</script>中的defer被浏览器完全忽略
什么时候可以放心用 async
async 仅适用于真正自包含、不碰 DOM、也不被其他脚本依赖的脚本。
- 典型例子:
analytics.js(只调navigator.sendBeacon)、ad-banner.js、error-tracker.js -
async脚本下载完立刻执行,可能发生在都没解析到一半的时候,document.getElementById('main')返回null是常态 - 多个
async脚本谁先下完谁先跑,顺序不可控——所以别把jquery.js和plugin.js都设成async - 如果同时写了
<script async defer src="a.js"></script>,现代浏览器会直接忽略defer,按async行为执行
type="module" 隐式 defer 但有硬约束
type="module" 脚本默认行为等同于 defer,但路径和执行逻辑更严格。
- 模块脚本自动延迟到 DOM 解析完成后执行,且按顺序,无需显式写
defer - 但模块路径必须是相对或绝对 URL(不能是纯文件名),且默认启用 CORS;
import的模块也必须满足同源或正确配置 CORS - 若需异步加载模块又想控制时机,可用
import()动态导入,它返回 Promise,适合按需触发 -
<script type="module" src="x.js" async></script>是合法组合,但async在这里仅影响加载启动时机,执行仍受模块系统约束
动态插入 script 是唯一能绕开 HTML 顺序限制的方案
当脚本不该随页面一起加载,而是由用户行为或运行时条件触发时,必须用 JS 动态插入。
- 常见用例:评论区滚动到底部才加载、地图组件点击后引入、A/B 测试分流加载不同 SDK
- 关键控制点:必须手动监听
onload和onerror,否则加载失败静默,调试困难 - 容易踩坑:重复插入同一 URL 的脚本,导致函数重定义、事件重复绑定;需加去重逻辑(如检查
document.querySelector('script[src="xxx.js"]')是否已存在) - 注意:动态插入的脚本默认行为等同于
async=true;若想模拟defer效果,可显式设置script.async = false,但必须确保插入时机合理(比如插入到后且 DOM 尚未开始解析)
真实项目里常混合使用 defer、async 和动态脚本,但它们之间没有执行优先级约定——defer 脚本还没执行完,一个 async 脚本就可能已经跑完了,而动态插入的脚本又可能插在中间。这种时间线交叉最容易被忽略,也是线上偶发 $ is not defined 或事件绑定失效的根源。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











