defer脚本能安全操作DOM,因其执行时机被锁定在HTML解析完成、DOMContentLoaded触发前,此时DOM树已就绪;async脚本下载完立即执行,不等DOM构建完成,故常导致document.getElementById("app")返回null。

脚本要操作 DOM,必须用 defer;脚本只上报、打点、加载广告,才考虑 async。选错一个,document.getElementById("app") 就返回 null。
为什么 async 脚本里经常拿不到 DOM 元素
因为 async 脚本下载完就立刻中断 HTML 解析执行,此时 DOM 树大概率没建完。document.body 可能还是 null,document.querySelector("#hero") 自然找不到——不是脚本慢,是它跑得太早。
- 常见错误现象:
Cannot read property 'appendChild' of null、TypeError: Cannot addEventListener to null - 哪怕脚本放在
<body>底部,async仍可能在<body>开始解析前就执行(尤其小脚本+快网络) - 它不等任何东西:不等 DOM、不等其他脚本、不等你写的初始化逻辑
- 适用场景极窄:仅限
gtag.js、taboola.js、error-tracking.js这类纯上报、无 DOM 依赖、无外部依赖的脚本
defer 脚本为什么能安全操作 DOM
defer 把执行“锁死”在两个节点之间:HTML 解析完成之后、DOMContentLoaded 触发之前。此时整个 DOM 树已就位,所有元素都可访问。
- 多个
defer脚本严格按 HTML 中书写顺序执行,vue.js一定在app.js前运行 - 即使
app.js比utils.js下载快,也得排队等前者执行完——顺序不被网络速度干扰 - 它不阻塞解析,但会延迟
DOMContentLoaded:某个defer脚本里有死循环,整个页面就卡住不动 -
type="module"脚本默认就是defer行为,手动加defer属性无效
defer 和 async 同时写会发生什么
浏览器只认 async,defer 被静默忽略。这不是 bug,是规范明确的行为。
立即学习“前端免费学习笔记(深入)”;
- 写成
<script async defer src="a.js"></script>→ 等同于只写了async - 内联脚本加
defer或async完全无效:<script defer>init();</script>里的init()依然立刻执行 - 两者共存于同一页面时,
async脚本可能在defer队列中间插队执行,导致依赖错乱 - Webpack/Vite 构建产物若已是
type="module",再加defer属于冗余操作,不会改变行为
服务端渲染(SSR)页面里最容易踩的坑
SSR 注入的初始数据(比如 window.__INITIAL_STATE__)必须在 defer 脚本执行前就存在,否则业务脚本读不到状态。
-
<script>window.__INITIAL_STATE__ = {...}</script>必须放在所有defer脚本之前,且不能加defer - 误把初始化逻辑写成同步内联脚本,会导致首屏白屏时间变长——应改为外链 +
defer - 动态加载(如点击后加载地图 SDK)别依赖
defer或async,用document.createElement('script')更可控 - IE9+ 支持
defer,但老企业内网若需兼容 IE8,这两个属性都不可用,得回退到DOMContentLoaded或window.onload包裹



















