defer仅对外部脚本生效,必须同时满足src存在且defer为无值布尔属性;其执行时机在dom构建完成、domcontentloaded之前,按html顺序执行,可安全操作dom。

defer 属性只对外部脚本生效,且必须同时满足 src 存在 + defer 作为布尔属性出现;其他任何写法都无效,浏览器既不报错也不提示,只会静默退回到默认行为。
为什么 document.getElementById("app") 在 defer 脚本里能用,但放在普通 <script></script> 里常是 null?
因为 defer 脚本的执行时机被明确限定在「HTML 解析完成、DOM 树构建完毕后,DOMContentLoaded 事件触发前」。此时所有静态 HTML 元素(包括 <div id="app">)已挂载进 DOM,可直接访问。
<ul>
<li>普通 <code><script src="a.js"></script> 会阻塞 HTML 解析:脚本下载+执行完,解析才继续 → 若脚本写在 里,#app 还没解析到,自然取不到
null
defer 和 async 混用时,defer 直接失效
只要一个 <script></script> 标签里同时写了 async 和 defer,浏览器会完全忽略 defer,按 async 规则处理:谁先下载完谁先执行,顺序不保证。
- ❌
<script src="utils.js" async defer></script>→ 等价于只写async - ✅ 多个有依赖的脚本,应统一用
defer:<script src="lodash.js" defer></script>+<script src="app.js" defer></script> - ⚠️ 构建工具(如 Vite)开启
build.inlineDynamicImports: true时,可能把外部脚本转成内联 →defer彻底失效,需检查最终生成的 HTML
哪些写法看似合理,实际根本不会触发 defer 行为?
这些是线上项目中最常被误写的“伪 defer”,它们和没写 defer 一样阻塞或乱序:
- 内联脚本加
defer:<script defer>console.log('hi')</script>→ 浏览器无视defer,立即执行 -
defer赋值:<script src="a.js" defer></script>或<script src="a.js" defer></script>→defer是布尔属性,只认有无,不认值 - 用了
document.write()的脚本 → defer 被浏览器主动忽略(该方法本身已废弃) - 模块脚本(
type="module")→ 默认自带 defer 语义,再加defer无意义,也无效
真正起作用的只有两个硬性条件:src 属性存在 + defer 以无值布尔形式出现。其余所有变体,包括位置放错、和其它属性共存、构建阶段被重写,都会让这个行为消失——而你很难从控制台或网络面板里发现它。











