async脚本下载不阻塞HTML解析但执行不等DOM就绪,易致getElementById返回null;仅适用于完全自包含、不操作DOM、无依赖的统计类脚本,业务脚本应改用defer。

async 属性不能让脚本“安全地异步运行”,它只让下载不阻塞 HTML 解析,但执行时机不可控——用错会直接导致 document.getElementById 返回 null、addEventListener 报错或依赖未定义。
async 脚本一下载完就执行,根本不管 DOM 是否就绪
这是最常被误解的点。很多人以为加了 async 就等于“等页面加载完再跑”,实际完全相反:async 脚本一旦下载完成,立刻中断当前 HTML 解析并执行。此时 <body> 可能才解析到一半,#submit-btn 根本不存在。
- 典型错误现象:
Uncaught TypeError: Cannot read property 'addEventListener' of null - 哪怕脚本只有 1KB、CDN 就在隔壁机房,也无法保证它比
<button id="submit-btn">先出现在 DOM 中 - IE10+、Chrome、Firefox 行为一致:不保序、不等 DOM、不延迟执行
- 修复方式不是“调大超时”,而是必须主动检查 DOM 状态,例如用
document.readyState === 'complete'或监听DOMContentLoaded
哪些脚本真能放心用 async?
只有三类脚本适合 async:完全自包含、不读写 DOM、也不被其他脚本依赖。它们失败不影响主流程,执行时机也无所谓。
-
analytics.js:只调navigator.sendBeacon()或fetch()上报,不碰document -
error-tracker.js:全局window.onerror监听,初始化后就挂起,不查节点 -
hotjar.js或fullstory.js:SDK 自行判断是否注入,不依赖外部变量 - ❌ 别把
jquery.js和app.js都设async:谁先下完谁先跑,$可能还没定义就执行app.js
async 和 defer 混用会破坏执行顺序
一个页面里同时存在 async 和 defer 脚本,等于把调度权交给两个互不通信的机制。结果往往是 init.js 在 lib.js 加载前就执行,ReferenceError: myUtils is not defined。
立即学习“前端免费学习笔记(深入)”;
-
<script src="lib.js" defer></script>→ 等 DOM 解析完、按序执行 -
<script src="init.js" async></script>→ 下完立刻执行,可能早于lib.js的任何阶段 - 现代浏览器遇到
<script async defer src="a.js"></script>会直接忽略defer,按async处理 - 真实项目中更稳妥的做法是:业务脚本统一用
defer,监控/统计类脚本用async,绝不交叉
动态插入 script 默认就是 async 行为,但要注意顺序和错误处理
用 JS 动态创建 <script>(比如点击后加载地图 SDK),默认等效于 async=true,无需手动设属性。但关键细节极易遗漏:
- ✅ 正确顺序:
s.src = 'map.js'→s.onload = initMap→document.head.appendChild(s) - ❌ 错误顺序:
document.head.appendChild(s)→s.src = 'map.js'→ 某些旧版 Chrome 仍会同步阻塞 - 必须监听
onerror,否则脚本 404 或 CSP 拒绝时静默失败,调试无从下手 - 重复插入同一 URL 脚本会导致函数重定义、事件重复绑定,需提前检查
document.querySelector('script[src="map.js"]')
真正难的不是加 async,而是判断“这个脚本到底要不要等 DOM”——如果它要查 document.body、绑按钮、渲染组件,那就别碰 async,老实用 defer 或封装进 DOMContentLoaded 回调里。否则,快是快了,但交互直接失效。



















