模块脚本多次引入同一URL只执行一次,因浏览器ES模块规范强制维护Module Map缓存,以解析后绝对URL为键,首次加载执行并缓存导出绑定,后续导入直接复用;路径差异、查询参数或跨域问题会导致视为不同模块而重复执行。

模块脚本在 HTML 中多次引入同一个 URL 文件时只执行一次,这不是“优化”或“可选行为”,而是浏览器 ES 模块规范强制保证的底层机制——每个模块标识符(specifier)对应唯一的 Module Record,整个页面生命周期内仅初始化并执行一次。
为什么同一模块不会重复执行
浏览器内部维护一个模块记录表(Module Map),以解析后的绝对 URL 为键。只要 import 路径经解析后指向同一文件(例如 ./utils.js 和 ./utils.js),无论在多少个 <script type="module"> 中被引用,都命中同一个记录:
- 首次加载时:读取源码 → 解析 import/export → 执行顶层代码 → 缓存导出对象
- 后续导入时:跳过加载与执行,直接复用已缓存的导出绑定(live binding)
- 即使多个模块同时 import 同一文件,它们共享的是同一内存实例,不是副本
哪些情况会触发重复执行
看似相同,但因路径解析结果不同,会被视为不同模块:
-
import './utils.js'与import './utils'(缺少 .js 后缀,解析失败或指向不同资源) -
import '/utils.js'与import './utils.js'(绝对路径 vs 相对路径,解析为不同 URL) - 带查询参数的 URL:
import './config.js?v=1'和import './config.js?v=2'(URL 不同,缓存键不同) - 跨域但未配置 CORS 的模块:加载失败,可能触发重试或降级逻辑,但成功加载后仍遵守单例
如何验证和利用这一机制
可在模块顶层添加 console.log 或副作用代码,观察是否仅输出一次;更实用的是将它作为天然单例基础设施使用:
立即学习“前端免费学习笔记(深入)”;
- 数据库连接、API 客户端、状态管理器等全局唯一对象,直接导出实例即可
- 避免在模块顶层做不可逆操作(如
document.body.append(...)),除非你明确依赖“仅一次”语义 - 测试中若需重置模块状态,现代浏览器暂不支持清除 Module Record,可改用动态
import()配合时间戳或随机参数绕过缓存
和传统 script 的关键区别
普通 <script src="a.js"></script> 每出现一次就执行一次,容易导致函数重定义、变量覆盖、初始化重复等问题;而模块脚本无需手动去重或加锁,单例是默认且不可关闭的行为:
- 没有
if (!window.MyLib) { ... }这类防御性判断的必要 - 不需要 IIFE 或闭包模拟单例,模块系统已原生提供
- 作用域隔离 + 单例执行 + 实时绑定,三者共同构成可预测的模块化基础



















