defer属性仅对外部脚本(含src)生效,内联脚本加defer会被忽略;其执行时机在dom构建完毕后、domcontentloaded前,保证顺序且不阻塞解析。

defer 属性只对外部脚本有效
如果你给一个内联 <script>console.log(1)</script> 加上 defer,它会被浏览器忽略——根本不会延迟,也不会报错,只是当没写。只有带 src 的外部脚本(比如 <script src="app.js" defer></script>)才真正触发 defer 行为。
常见错误是误以为加了 defer 就能控制任意脚本执行时机,结果发现内联逻辑还是在 HTML 解析中途就跑了,破坏了 DOM 就绪预期。
- 必须同时满足:有
src属性 + 有defer属性 - 不支持
document.write()—— defer 脚本里调用会直接报错或静默失败 - 多个
defer脚本按出现顺序执行,这点和async不同
defer 和 DOMContentLoaded 的关系很紧密
defer 脚本的执行时机,严格来说是在 HTML 解析完成、DOM 构建完毕之后,但在 DOMContentLoaded 事件触发之前。也就是说,你在 defer 脚本里可以安全访问 document.body、querySelector 等,不需要额外监听事件。
但要注意:如果页面里还有其他非 defer 脚本(比如没有 src 的内联脚本、或带 async 的脚本),它们可能提前修改 DOM 或注册监听器,导致 defer 脚本看到的 DOM 状态不是“纯解析后”的原始状态。
- defer 脚本不阻塞 HTML 解析,也不阻塞渲染
- 它比
DOMContentLoaded稍早一点执行,但你可以把它当成“等 DOM 就绪后立刻执行”来用 - 不要依赖
window.onload,那是等资源(图片、样式表)都加载完才触发
defer 和 async 的关键区别不止是执行时机
最常被混淆的是:都“不阻塞解析”,但 defer 保证顺序执行且等 DOM 就绪,async 是谁先下载完谁先执行,完全不管顺序,也不等 DOM。
典型踩坑场景:你有两个互相依赖的外部脚本 a.js(定义工具函数)和 b.js(调用那些函数),如果都加 async,b 可能先执行而报 undefined is not a function;换成 defer 就稳了。
-
async:适合完全独立的脚本(如统计代码、广告 SDK) -
defer:适合需要操作 DOM 或依赖前序脚本逻辑的业务代码 - 两者不能共存于同一个
<script></script>标签——async优先级更高,存在即忽略defer
现代打包工具里 defer 很容易被绕过
Webpack/Vite/Rollup 默认生成的 HTML 模板里,往往把 JS 插入为内联 <script></script> 或使用 async,即使你手动加了 defer,也可能被插件覆盖。比如 Vite 的 build.rollupOptions.output.entryFileNames 配置不影响 HTML 注入方式;真正起作用的是 build.inlineDynamicImports 或插件如 vite-plugin-html 的模板逻辑。
验证是否生效最简单的方法:打开 DevTools → Network → 找到 JS 请求 → 查看 Initiator 列,如果是 parser 说明是解析时触发的(defer/normal),如果是 script 说明是 JS 动态创建的(绕过了 defer)。
- 检查最终生成的 HTML 源码,确认
<script src="xxx.js" defer></script>确实存在且没被删掉 - 避免在
index.html里用<script type="module"></script>—— 它自带类似 defer 的行为,但和传统 script 的执行队列不混排 - 服务端渲染(SSR)场景下,首屏 JS 如果由框架注入,很可能根本不走 HTML 的 defer 流程
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











