modulepreload 不能打断递归加载,它仅提前触发模块及其依赖的 fetch 和 parse,不改变执行顺序;需手动预加载所有关键路径模块,且只对静态 import 生效。

modulepreload 是什么,它真能打断递归加载吗
不能。rel="modulepreload" 本身不打断任何加载链路,它只是提前触发模块及其依赖的 fetch 和 parse,但不会改变模块图的拓扑顺序或执行时机。它解决的是「等前面模块执行完、发现 import 后才开始拉依赖」这个延迟问题,而不是递归深度问题。
典型误用场景:给 main.js 加 rel="modulepreload",但它内部 import './utils.js' 的依赖仍需等 main.js 执行到那一行才 resolve——除非你显式地也 preload utils.js。
- 必须手动列出所有关键路径上的模块(包括深层嵌套的依赖),
modulepreload不会自动爬取 import 语句 - 只对
<script type="module"></script>生效,对type="importmap"或动态import()无预加载效果 - 浏览器按资源 URL 去重,同一 URL 多次 preload 只触发一次 fetch
怎么写有效的 modulepreload Link 标签
最简有效写法:<link rel="modulepreload" href="./a.js">,但实际中容易漏掉几个关键点:
-
href必须是完整路径(相对或绝对),不能是 bare specifier(如lodash),否则 404;需和 import map 中的映射一致,或直接用解析后的 URL - 如果模块用了
import.meta.url或new URL('./x.js', import.meta.url)动态构造路径,preload 的href必须匹配最终解析结果(比如带 hash 的构建产物) - 建议加
crossorigin属性(即使同源),否则跨域模块会因 CORS 策略被拒绝 preload - 不要在
里放modulepreload,它必须在且早于首个<script type="module"></script>
示例:
<link rel="modulepreload" href="/assets/main.abc123.js" crossorigin><link rel="modulepreload" href="/assets/utils.def456.js" crossorigin><link rel="modulepreload" href="/assets/api.ghi789.js" crossorigin>
如何自动化生成 modulepreload 列表
手写易漏、难维护,尤其在 vite/webpack 构建后有 hash 的场景。核心思路是:从入口模块出发,静态分析 AST 或利用构建插件提取所有静态 import 路径。
- vite 用户可启用
build.modulePreload: { polyfill: false }(默认开启),vite 会自动生成并注入modulepreload标签——但仅限直接依赖,不包含动态 import 或条件分支里的模块 - rollup 用户可用
rollup-plugin-module-preload,配合output.manualChunks配置,导出依赖图 - 自己解析时注意:ESM 静态 import 是编译期确定的,但
import('./x.js')是运行时行为,无法被 preload 覆盖;这类模块只能靠import()前预 fetch(非 preload) - 检查是否生效:打开 Network 面板,筛选
Initiator列为Preload,确认目标模块确实在 HTML 解析阶段就开始下载
modulepreload 和 defer、async、type="module" 的关系
它们不是互斥选项,但优先级和行为差异明显:
-
modulepreload只做 fetch + parse,不执行;而<script type="module"></script>默认 deferred(等 DOM 解析完才执行),且有严格的执行顺序(按出现顺序 + 拓扑排序) - 加了
modulepreload的模块,仍要通过正常import或<script type="module"></script>引入才会执行;preload 不等于 auto-execute - 不要对同一个模块同时用
modulepreload和async——async会破坏模块执行顺序,而 preload 的收益在 async 场景下基本归零 - 如果主模块是
<script type="module" src="main.js"></script>,它的依赖链越深,越需要把中间层模块(比如 shared/utils.js)单独 preload,否则首屏 JS 执行仍卡在逐层 import 的网络等待上
真正起效的组合是:preload 关键路径模块 + 正常 type="module" 引入 + 保持 import 语句结构不变。
复杂点在于,你得知道哪些模块在关键路径上——这没法靠工具完全推断,得结合 performance profile 和代码分割策略人工判断。比如一个 import('./chart.js') 虽然延迟加载,但如果它在用户点击后 100ms 内必须渲染,那它就该被 preload(哪怕用 link + fetch + cache 的方式绕过 modulepreload 限制)。











