async脚本下载完立即执行,可能早于DOM解析完成,导致document.getElementById返回null;defer则确保在DOM解析完毕后、DOMContentLoaded前按序执行。

直接用 async 属性就能让脚本异步加载,但它不是万能解药——用错位置或场景反而导致 document.getElementById 找不到元素、执行时机失控。
async 脚本为什么可能比 DOM 还早执行?
浏览器遇到 <script async src="app.js"></script> 时,会立刻在后台下载,一旦下载完成就**立即执行**,不管 <body> 解析到哪了。如果脚本里写了 document.getElementById('main'),而此时 <div id="main"> 还没被解析出来,就会返回 null。
- 常见错误现象:
Uncaught TypeError: Cannot read property 'addEventListener' of null - 使用场景:适合完全不操作 DOM、也不依赖其他脚本的独立逻辑,比如埋点统计、广告 SDK
- 容易踩的坑:把
async脚本放在<head>里却指望它能访问<body>中的节点 - 性能影响:确实不阻塞 HTML 解析,但执行时会中断渲染,造成短暂卡顿
defer 才是大多数业务脚本的默认选择
defer 的行为更可预测:它也异步下载,但执行被严格推迟到整个 HTML 解析完成、DOMContentLoaded 事件触发前。所有 defer 脚本按它们在文档中出现的顺序执行。
- 常见错误现象:多个库混用时(如先
jquery.js后plugin.js),用了async导致plugin.js先执行报$ is not defined - 使用场景:依赖 DOM 或依赖其他脚本的代码,比如初始化表单验证、绑定按钮事件
- 参数差异:
defer不接受任何值,写成defer="true"或defer="defer"是无效的,只写defer即可 - 兼容性:IE10+ 支持,现代项目基本无顾虑
动态创建 script 标签更适合条件加载
当脚本不该随页面一起加载,而是等用户点击、滚动到底部、或某个模块可见时才加载,就得用 JS 动态插入:document.createElement('script')。
- 常见错误现象:重复插入同一 URL 的脚本,导致函数被定义两次、事件被绑定多次
- 使用场景:评论区懒加载、地图组件按需引入、A/B 测试脚本分流
- 关键控制点:必须手动监听
onload和onerror,否则失败静默,调试困难 - 注意点:
script.async = false可模拟defer行为,但得确保插入时机合理(比如插入到<head>后)
ES6 模块天然带 defer 特性,但要注意 type="module"
写成 <script type="module" src="main.mjs"></script> 后,浏览器自动按 defer 方式处理:异步下载 + HTML 解析完后执行 + 顺序保证。但它还有额外约束:
- 常见错误现象:直接双击打开 HTML 文件(
file://协议),模块脚本因跨域限制报CORS错误 - 使用场景:现代前端工程(Vite、Webpack 构建产物)、需要
import/export语法的代码 - 参数差异:加
async属性后行为变回类似普通async——下载完立刻执行,顺序不再保证 - 兼容性:Safari 10.1+、Chrome 61+、Firefox 60+,旧版 IE 完全不支持
真正容易被忽略的是:async 和 defer 都不能解决「脚本内容本身耗时」的问题。一个 500KB 的 async 脚本,下载快但执行慢,照样卡主线程。优化要从拆包、代码分割、减少同步计算三方面下手,而不是只盯着 async 标签打转。

















