必须用 defer:脚本需操作 dom 或存在加载顺序依赖;可放心用 async:脚本自包含、不读写 dom 且无依赖;动态插入适用于按需加载;type="module" 隐式 defer 但有路径和执行约束。

用 async 还是 defer,不看“要不要快”,只盯住两件事:脚本是否读写 DOM,以及是否依赖其他脚本执行结果。错配会导致 document.getElementById 返回 null、$ is not defined 或事件绑定失效——这不是加载慢的问题,是执行时机崩了。
什么时候必须用 defer
脚本要操作 DOM 或存在加载顺序依赖,defer 是唯一安全选择。
- 典型场景包括:初始化
#app挂载点(Vue/React 入口)、绑定按钮点击事件、调用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)、广告 SDK(ad-banner.js)、错误监控(error-tracker.js),它们只调navigator.sendBeacon或fetch上报,不读document.body,不绑事件,失败也不影响主逻辑 -
async脚本下载完立刻执行,可能发生在都没解析到一半的时候,document.getElementById('main')返回null是常态 - 多个
async脚本谁先下完谁先跑,顺序不可控——所以别把jquery.js和plugin.js都设成async -
注意:如果同时写了
<script async defer src="a.js"></script>,现代浏览器会直接忽略defer,按async行为执行
动态创建 script 标签的适用场景
当脚本不该随页面一起加载,而是由用户行为或运行时条件触发时,必须用 JS 动态插入。
- 常见用例:评论区滚动到底部才加载、地图组件点击后引入、A/B 测试分流加载不同 SDK
- 关键控制点:必须手动监听
onload和onerror,否则加载失败静默,调试困难 - 容易踩坑:重复插入同一 URL 的脚本,导致函数重定义、事件重复绑定;需加去重逻辑(如检查
document.querySelector('script[src="xxx.js"]')是否已存在) -
注意:动态插入的脚本默认行为等同于
async=true;若想模拟defer效果,可显式设置script.async = false,但必须确保插入时机合理(比如插入到后且 DOM 尚未开始解析)
type="module" 脚本的隐式 defer 行为
ES 模块脚本天然具备 defer 特性,但有额外约束,不能简单当成“更高级的 defer”来用。
-
<script type="module" src="main.mjs"></script>会自动异步下载 + HTML 解析完后执行 + 顺序保证,无需显式写defer - 但它要求模块路径必须是合法 URL(不支持相对路径如
./utils.js在非服务器环境下双击打开会失败) - 模块内使用
import()动态导入的代码,不受type="module"的延迟约束——这部分执行时机仍由 JS 引擎按需决定,需自行兜底 -
注意:如果你的入口脚本是模块,又依赖服务端注入的
window.__INITIAL_STATE__,这个变量必须在该<script type="module"></script>执行前就已存在,且不能加任何加载属性干扰顺序
最容易被忽略的是:所有这些机制(async、defer、type="module")都只作用于带 src 的外部脚本;而真实项目里,常有人把初始化配置写成内联 <script defer></script>,结果白加——浏览器根本不认。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











