async和defer仅对带src的外部脚本有效;内联脚本加二者无效且不报错,仍阻塞。defer脚本拿不到document.body是因执行时body未生成,需提前声明节点或用DOMContentLoaded监听;async脚本不应操作DOM,适合无依赖逻辑,强需时可用waitForElement等方案兜底。

直接结论:async 和 defer 是仅对带 src 的外部脚本生效的合法解法;内联脚本加这两个属性完全无效,且不会报错——它就当没看见,照常阻塞。
为什么 defer 脚本里还拿不到 document.body
这不是 defer 失效,而是你把脚本放在了 <head> 里,而 HTML 主体极短(比如只有 <body></body>),导致解析器还没来得及生成 body 元素,defer 脚本就已经执行了。
- 确认目标节点是否已在 HTML 中提前声明,例如用
<div id="app"></div>替代通篇依赖document.body - 避免在
defer脚本开头写document.body.appendChild(...)这类强依赖 - SSR 场景下,服务端注入的
window.__INITIAL_STATE__必须出现在所有defer脚本之前,且不能加defer - 如果必须操作
body,改用document.addEventListener('DOMContentLoaded', ...)显式等待
async 脚本里 document.getElementById 返回 null 怎么兜底
因为 async 下载完立刻执行,不等 DOM 构建完成,此时页面可能连 <body> 都还没解析出来。
- 不要在
async脚本里直接操作 DOM;适合它的只有统计、埋点、广告等无 DOM 依赖逻辑 - 若业务强需,改用
document.querySelector+mutationObserver监听目标节点出现,或封装一个waitForElement工具函数 - 切忌在
async脚本后紧跟内联调用,如<script src="umami.js" async></script><script>umami.track();</script>—— 后者同步执行,必报错 - 第三方 SDK 文档明确写了“支持异步加载”的才可放心用
async;否则默认按defer处理更稳妥
哪些写法看似优化实则白忙
很多“看起来不阻塞”的写法,其实根本没绕过浏览器解析规则。
立即学习“前端免费学习笔记(深入)”;
-
<script src="a.js"></script>放在</body>前 ≠ 不阻塞:只要没加async或defer,它仍会暂停 HTML 解析,只是卡点挪到了最后 - 给内联脚本加
defer或async:浏览器直接忽略,该阻塞还是阻塞 -
<link rel="stylesheet">搬到<body>底部:CSS 仍是渲染阻塞资源,位置不影响阻塞行为 - 混用
defer和async:比如a.js加defer、b.js加async,b 可能先执行并访问 a 尚未定义的变量
最容易被忽略的复杂点是:defer 保序但不保环境,async 不保序也不保时机,而浏览器对内联脚本和动态插入脚本的处理逻辑完全不同——这些边界情况不出现在本地开发,往往只在线上流量高峰或低配设备上突然暴露。



















