async脚本执行时DOM可能未就绪,导致document.getElementById返回null;它不等待HTML解析完成,下载完立即中断解析执行,故常在head中操作body节点时报错。
async 属性让外部脚本异步下载、下载完立即执行,但不保证执行顺序,也不等 DOM 就绪——它不是“无感加载”,而是“抢时机执行”。用错地方会直接导致 document.getElementById 返回 null 或 $ is not defined。
因为 async 不等 HTML 解析完。浏览器遇到 <script src="app.js" async></script>,一边下载 app.js,一边继续解析页面;一旦下载完成,立刻中断解析、执行脚本。此时若脚本里写了 document.getElementById('header'),而 <div id="header"> 还没解析到,结果就是 null。
Uncaught TypeError: Cannot read property 'addEventListener' of null
<head> 里,却在脚本中操作 <body> 下的节点console.log(document.body === null),大概率输出 true
看脚本是否依赖 DOM 或其他脚本。
async:脚本完全独立,不读 DOM、不调其他 JS、失败不影响主流程(如 analytics.js、ads.js)defer:要操作 document、绑定事件、或和前一个 src 脚本有依赖(如先 jquery.js 后 plugin.js)async 和 defer 同时存在时,浏览器按规范忽略 defer,只认 async
<script defer>console.log(1)</script> 中的 defer 会被浏览器忽略,只对带 src 的外部脚本有效会。虽然下载不阻塞 HTML 解析,但执行时浏览器必须暂停解析、交出主线程,直到脚本运行完。如果脚本里有大量同步计算或长任务,用户会感知到短暂卡顿,首屏渲染时间(LCP)可能被拉长。
立即学习“前端免费学习笔记(深入)”;
defer:defer 执行时 DOM 已就绪,且多个 defer 脚本按序排队,更利于主线程调度setTimeout(() => { ... }, 0) 延后执行async 在 IE9 及以下完全不支持,现代项目虽可忽略,但若需兼容旧企业内网环境,得 fallback 到动态创建 script 标签真正难处理的不是 async 本身,而是脚本内部隐式依赖:比如某个统计 SDK 看似独立,实则内部调用了 performance.getEntries(),而这个 API 在某些低端 Android WebView 中需 DOM 就绪后才稳定可用——这种边界情况,光靠属性声明解决不了,得结合 onload 回调和特征检测兜底。