能,defer脚本在dom构建完毕后、domcontentloaded前执行,可安全访问document.body等节点,无需监听事件;但仅对带src的外部脚本生效,内联脚本加defer会被忽略。

defer 脚本能安全访问 document.body 吗?
能,而且无需额外监听事件——只要脚本是外部的、带 src、且只写了 defer(无值),它就一定在 DOM 树完全构建完毕后执行,早于 DOMContentLoaded,但晚于 HTML 解析结束。
常见错误现象:
-
document.getElementById("app")返回null,其实是脚本没加defer或写成了内联 - 本地快、线上慢时偶发失败,本质是没加
defer导致执行时机飘移 - 写了
<script defer>console.log(document.body)</script>—— 内联脚本加defer被浏览器完全忽略,document.body仍是undefined
真正起效的唯一写法是:<script src="main.js" defer></script>。此时你可以放心调用 document.querySelector、addEventListener、document.body.appendChild 等,不会报错。
多个 defer 脚本为什么不会乱序?
因为执行顺序与下载完成时间无关,只取决于它们在 HTML 中的声明位置。浏览器会把所有带 defer 的脚本缓存起来,等 DOM 构建一完成,就按源码顺序依次执行。
使用场景:
- 依赖链明确:比如
vendor.js提供$,app.js里用了$("#header") - 初始化逻辑分层:先加载工具函数,再初始化组件,最后绑定事件
注意点:
-
<script src="a.js" async defer></script>——async优先级更高,defer被静默丢弃 - Webpack 默认不加
defer,需在html-webpack-plugin中显式配scriptLoading: 'defer' - Vite 默认生成内联脚本,
defer无效;得关掉inlineDynamicImports或改用build.rollupOptions.output.entryFileNames输出外链
defer 和 DOMContentLoaded 到底谁先谁后?
defer 脚本执行完,才触发 DOMContentLoaded。换句话说,你在 defer 脚本里注册的 DOMContentLoaded 监听器,一定不会被提前触发。
但它不等资源加载:图片、CSS、字体、fetch 请求都还没开始或仍在进行中。所以:
- 可以安全操作 DOM 节点、设置样式、挂载事件
- 不能假设
document.images[0].complete === true - 不能依赖
getComputedStyle拿到最终渲染尺寸(CSS 可能未生效) -
window.onload更晚,要等所有资源(含图片)加载完才触发
一个典型误判:defer 脚本里用 import("./model.bin") 加载模型文件,结果发现 model.bin 还在 fetch 中——defer 只管 JS 执行时机,不管动态导入的网络请求。
为什么加了 defer 却还是找不到元素?
最常被忽略的是:页面里混入了其他非 defer 脚本。比如 <script>initHeader()</script>(内联)、或 <script async src="analytics.js"></script>,它们可能提前修改了 DOM,导致你的 defer 脚本看到的不是“原始解析后”的 DOM 状态。
另一个硬伤是构建工具绕过:Rollup 插件默认不加 defer,Vite 在 dev 模式下注入的 HMR 脚本是内联的,defer 对它完全无效。
验证方法很简单:打开 DevTools → 查看页面源码(不是 Elements 面板),确认最终输出的 HTML 确实是 <script src="xxx.js" defer></script>,而不是被构建流程抹掉了或转成了内联。
真正容易被忽略的边界是:defer 保证 DOM 就绪,但不保证你脚本里依赖的任何异步资源就绪——那得靠你自己用 Promise.all、load 事件或状态标记来兜底。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











