defer仅对外部脚本(含src属性)生效,内联脚本加defer无效;多个defer脚本严格按HTML书写顺序执行,与下载快慢无关;type="module"脚本天然defer,无需重复添加。

defer只对带src的外部脚本生效,内联脚本加defer无效
浏览器根本不会把 <script defer>init();</script> 当作 defer 脚本处理——它和没写 defer 一样,立即执行、阻塞 HTML 解析。真正起作用的只有两个硬性条件:src 属性必须存在,且 defer 必须是无值布尔属性(即不能写成 defer="true" 或 defer="false")。常见误写包括:<script src="" defer></script>(空 src)、<script src=" " defer></script>(空格 src)、<script src="a.js" defer="defer"></script>——这些全部退回到同步行为,浏览器不报错也不提示。
多个defer脚本严格按HTML中书写顺序执行,与下载快慢无关
这是解决依赖问题的核心机制。比如你写了:
<script src="utils.js" defer></script><br><script src="app.js" defer></script>
即使 app.js 比 utils.js 先下载完,浏览器也会等 utils.js 执行完毕再运行 app.js。常见错误包括:
- 把业务脚本写在工具库前面:
<script src="app.js" defer></script>放在<script src="lodash.js" defer></script>前 → 报ReferenceError: _ is not defined - 构建工具(如 Webpack 的
html-webpack-plugin)默认插入 polyfill 到<head>最前,无意中破坏了你手动排好的顺序 - 用
document.write()动态生成<script defer>标签 → 现代浏览器已禁用该方式,且动态脚本不继承defer语义
defer和async混用时,defer被静默忽略
只要同一个 <script> 标签里同时出现 async 和 defer,浏览器就按规范只执行 async 行为:<script src="utils.js" async defer></script> 等价于只写了 async。这意味着:
立即学习“前端免费学习笔记(深入)”;
- 执行顺序不再受 HTML 位置约束,谁先下载完谁先执行
-
utils.js可能在 DOM 构建中途就运行,导致document.getElementById("app")返回null - 如果页面里还有其他
defer脚本,它们会等到所有async脚本都跑完才开始执行,但彼此之间仍按顺序
别指望靠混合属性“兼顾两者”,这只会让依赖链断裂。
type="module"脚本天然具备defer语义,显式加defer冗余且可能出错
<script type="module" src="main.js"></script> 已隐含并行下载、DOM 就绪后执行、严格按声明顺序运行三重保障。再加 defer 不仅无效,还可能干扰构建工具(如 Webpack 模块分析器)对依赖关系的判断。另外要注意:
-
type="module"默认启用严格模式,且支持顶层await,迁移时需验证初始化逻辑是否兼容 - 它和普通
defer脚本不在同一执行队列:混用<script src="legacy.js" defer></script>和<script type="module" src="new.js"></script>时,两者执行时机接近但不严格同步,别让它们互相假设对方已完成 - 构建产物若已用
type="module",手动加defer是白费力气,最终 HTML 里看到的仍是冗余属性
真正容易被忽略的是:defer 只管脚本本身的执行时机,不管它内部的动态行为。哪怕 app.js 加了 defer,里面调用的 import("./data.json") 或 fetch("/api/config") 依然可能还在进行中——DOM 就绪 ≠ 业务数据就绪。



















