defer仅对外部脚本(含src属性)有效,执行于dom构建完成、domcontentloaded事件触发前,按html顺序执行;内联脚本、混用async、构建转内联等均导致其静默失效。

加 defer 能解决,但只对带 src 的外部脚本有效;内联脚本、混写 async、或构建阶段被转成内联,都会让 defer 彻底失效——浏览器不报错,只是静默退回到默认行为。
为什么 defer 脚本能避开 DOM 抢占
浏览器解析 HTML 是自上而下线性进行的。普通 <script src="a.js"></script> 遇到就暂停解析、下载并执行,此时 甚至还没开始读取,document.getElementById("app") 必然返回 null。defer 的核心机制是:脚本仍异步下载,但执行被推迟到整个 HTML 解析完成、DOM 树完全挂载之后,且在 DOMContentLoaded 事件触发前。
这意味着你能在 main.js 里直接写:
document.getElementById("app").innerHTML = "loaded";<br>document.querySelector("button").addEventListener("click", handler);
不用包 document.addEventListener('DOMContentLoaded', ...),也不用靠把脚本挪到 前这种手动维护方式。
哪些写法看似加了 defer,实际根本没生效
-
<script defer>init();</script>:内联脚本不支持defer,浏览器无视该属性,立即执行 -
<script src="a.js" defer></script>或defer="false":defer是无值布尔属性,赋值即无效 -
<script src="a.js" async defer></script>:async优先级更高,defer被静默丢弃,执行时机退化为“谁先下载完谁先跑” -
<script type="module" src="a.js" defer></script>:模块脚本默认自带defer语义,再加defer无效 - Vite 构建时开启
build.inlineDynamicImports: true:外部脚本被转成内联,defer失效
多个 defer 脚本的依赖顺序怎么保证
执行顺序严格按 HTML 中 <script defer></script> 标签的出现顺序,和下载完成时间无关。这对依赖链至关重要:
<script src="lodash.js" defer></script><br><script src="utils.js" defer></script><br><script src="app.js" defer></script>
即使 app.js 体积小、先下载完,也必须等 lodash.js 和 utils.js 执行完毕才轮到它。但要注意:
- 如果 HTML 里
app.js写在lodash.js前面,就会报ReferenceError: _ is not defined - 服务端注入的初始化数据(如
window.__INITIAL_STATE__)必须放在所有defer脚本之前,且自身不能加defer - 某个
defer脚本执行出错(比如语法错误),后续脚本仍会继续执行,不会中断队列
defer 不解决的问题,别指望它兜底
defer 只控制执行时机,不处理运行时失败:
-
404或 CSP 阻断导致脚本加载失败,defer不会重试或 fallback - 脚本里用了
document.write(),浏览器会直接忽略defer,甚至清空页面 -
defer不等图片、CSS、字体、fetch请求完成,所以img.naturalWidth或getComputedStyle可能拿不到最终值 - 动态导入(如
import("./model.bin"))的网络请求,defer完全不管
真正容易被忽略的是:构建工具可能悄悄改写你的 defer —— 检查最终生成的 HTML,确认 src 存在、defer 是无值布尔属性、且没被其他属性干扰。否则你写的不是 defer,只是个看起来像 defer 的普通脚本标签。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











