type="module" 脚本默认具有 defer 语义,因其底层加载机制按模块图调度,下载不阻塞 HTML 解析,执行在 DOM 构建完成、DOMContentLoaded 触发前,且严格按 HTML 中顺序执行,无需显式写 defer。

type="module" 脚本为什么不用写 defer 就自动延迟执行
因为浏览器把 type="module" 当作一个独立的加载上下文,它天然绑定 defer 语义——不是“模仿”,而是底层加载机制就按 defer 模式调度:下载不阻塞 HTML 解析,执行时机严格落在 DOM 构建完成之后、DOMContentLoaded 触发之前。
这和手动加 defer 的外部脚本行为高度一致,但有本质区别:模块脚本的“延迟”由模块图(module graph)驱动,依赖关系决定执行顺序,而非单纯靠 DOM 标签顺序排队。
- 多个
<script type="module">按 HTML 中出现顺序执行,哪怕它们是跨目录甚至跨域引入的 - 内联模块(
<script type="module">console.log('hi')</script>)也遵守该规则,而普通内联脚本加defer是被忽略的 - 它不等 CSS 加载完成,只等 DOM 树 ready;所以如果模块里用
document.querySelector取元素,一定能拿到,但取不到尚未解析的后续节点
type="module" 的 defer 行为和普通 defer 脚本混用时谁先执行
它们在同一个页面里共存时,执行顺序只看 HTML 中的书写位置,不是按“谁更 defer”来排序。也就是说:<script defer src="a.js"></script> 和 <script type="module" src="b.mjs"></script> 如果 a 在前、b 在后,a 一定先执行;反过来则 b 先执行。
- 两者都保证 DOM 就绪,且都不阻塞解析,所以混合使用不会破坏基础可用性
- 但模块脚本作用域隔离,a.js 里挂到
window上的函数,b.mjs 里访问不到——这不是执行顺序问题,是作用域问题 - 如果你在 a.js 里动态改了 DOM 结构(比如插入一个
<div id="app"></div>),b.mjs 能安全操作它;但如果 a.js 是 async,就可能还没执行完,b.mjs 就去查#app,结果是null
为什么加了 type="module" 还报 document is not defined 或 DOM 操作失败
大概率不是 defer 失效,而是脚本本身没等 DOM 就绪就运行了——最常见原因是:你写了内联模块但误用了 document.currentScript,或者路径写错导致模块根本没加载成功,浏览器 fallback 到执行空脚本或报错后静默跳过。
立即学习“前端免费学习笔记(深入)”;
-
document.currentScript在模块脚本里永远是null,不能靠它取当前<script>标签 - 路径没带
.js后缀(如import './utils')会导致整个模块加载失败,控制台报Failed to load module script,后续代码不执行 - 在
file://协议下双击打开 HTML 文件,模块脚本压根不会发起网络请求,控制台可能只显示笼统的 CORS 错误,实际是浏览器策略拦截 - 顶层
await是允许的,但如果你写了await fetch(...)却没处理 reject,也会中断模块执行流
想让模块脚本更快执行?别乱加 async
async 对 type="module" 不仅无效,还可能引发执行顺序混乱。模块加载器不认 async 属性,一旦写了,浏览器会降级为传统异步脚本逻辑——失去依赖顺序保证,import 语句直接报错,甚至整个模块被忽略。
- 真正提速方式只有两个:
<link rel="modulepreload">提前触发下载,或把关键模块拆成小块 + 动态导入(await import('./chunk.js')) -
modulepreload必须和后续<script type="module" src="...">的src完全一致(包括查询参数),否则不生效 - 跨域模块(如 CDN 上的库)必须加
crossorigin属性,否则预加载静默失败



















