defer只对带src的外部脚本生效;内联脚本或空src的script标签加defer会被忽略,仍同步阻塞解析;defer脚本在HTML解析完成、DOMContentLoaded触发前按声明顺序执行,不保证CSS/图片等资源就绪。

defer只对带src的外部脚本生效
浏览器遇到 <script src="app.js" defer></script> 时,会立刻发起下载,同时继续解析 HTML,不暂停、不等待执行。但如果你写的是 <script defer>init();</script> 或 <script src="" defer></script>,defer 会被静默忽略,脚本照常同步执行、阻塞解析。
常见错误现象:
- 控制台没报错,但
document.getElementById返回null - 本地快、线上慢时偶发白屏(DOM 没就绪就执行了)
- 把脚本挪到
</body>前能跑通,但破坏构建流程或模块组织
defer的执行时机不是“等DOM加载完”,而是“HTML解析完成之后”
defer 脚本在 DOM 树构建完毕、DOMContentLoaded 事件触发前执行——它本身就会参与触发该事件(如果它是最后一个推迟执行的脚本)。但它不等图片、iframe、CSS 等资源加载完成,只等 HTML 文本解析和 DOM 节点挂载结束。
这意味着你可以放心调用:
立即学习“前端免费学习笔记(深入)”;
document.querySelectorelement.addEventListenercanvas.getContext
但不能假设 document.images[0].complete 为 true,也不能依赖 getComputedStyle 获取渲染后样式(此时 CSSOM 可能未就绪)。
多个defer脚本严格按HTML中书写顺序执行
顺序由标签在 HTML 中的出现位置决定,与下载快慢、文件体积、网络延迟完全无关。比如:
<script src="lodash.js" defer></script> <script src="utils.js" defer></script> <script src="main.js" defer></script>
即使 main.js 先下载完,也必须等 lodash.js 和 utils.js 执行完才运行。这是 defer 区别于 async 的核心契约。
容易踩的坑:
- 把
main.js写在utils.js前面 → 报ReferenceError: utils is not defined - 构建工具自动注入 polyfill 到
<head>最前,无意中打乱你手动排好的依赖链 - 混用
async和defer:<script src="a.js" async></script><script src="b.js" defer></script>→a.js可能在 DOM 解析中途就执行,b.js却等到最后,依赖关系断裂
defer ≠ document.addEventListener('DOMContentLoaded', ...)
defer 是加载策略,影响下载时机和执行队列;DOMContentLoaded 是事件机制,只反映 DOM 构建状态。两者语义不同,不可互换。
关键差异:
-
defer脚本执行过程中若含死循环或长同步操作,会拖慢DOMContentLoaded触发 -
type="module"脚本默认具备defer行为,再加defer属冗余,部分构建工具可能误判依赖 - 动态插入的脚本(如
document.createElement('script'))即使设script.defer = true,老浏览器也不认,别依赖
真正起作用的只有两个硬条件:src 存在且非空,defer 属性存在(布尔属性,不赋值)。



















