modulepreload不会加速模块执行,只提前拉取网络资源;其与defer、async的本质区别在于:defer控制执行时机但不预下载,async异步下载并立即执行会破坏依赖顺序,而modulepreload仅在HTML解析早期发起fetch,不解析、不执行、不构建模块图。
modulepreload 不会让模块执行变快,它只提前拉取网络资源;写错路径、漏 crossorigin、没列子模块,就等于没写。
很多人加了 modulepreload 还在等模块执行,其实它根本不碰执行逻辑。浏览器拿到后只 fetch,不 parse、不构建 module graph、不执行任何代码。
defer:DOM 解析完才执行,但下载时机由 parser 决定,可能晚于首屏渲染async:下载完立刻执行,破坏模块顺序,无法用于有 import 依赖的链路modulepreload:只做一件事——在 HTML 解析早期发起 fetch,把模块和显式声明的依赖放进缓存,后续 <script type="module" src="..."> 才真正触发解析与执行常见静默失效,控制台不报错,Network 面板也看不到请求,问题基本锁定在这几个硬性条件上:
href 值必须和后续 <script type="module" src="..."> 的 src 完全一致——协议、域名、路径、查询参数(如 ?v=1.2.3)一个都不能差<link> 必须带 crossorigin 属性,否则浏览器直接忽略./main.mjs 或 ../utils.js 会被跳过,必须用绝对路径或根相对路径(如 /assets/main.mjs)modulepreload 不递归加载子模块——main.mjs 里 import './utils.js',你得手动再加一行 <link rel="modulepreload" href="/assets/utils.js" crossorigin>
手写容易漏、难维护,尤其构建产物带 hash 时。但理解规则才能 debug:
立即学习“前端免费学习笔记(深入)”;
<link rel="modulepreload"> 标签建议按执行依赖顺序排列:先 vendor.mjs,再 main.mjs,部分浏览器会复用 TCP 连接href 必须指向最终解析后的 URL(比如 Vite 构建后是 /assets/main.a1b2c3.js,不是 src/main.js)type="text/javascript" 的文件加 modulepreload 会静默失败<head> 中,且早于首个 <script type="module">;放在 <body> 里无效真实项目别手写——Vite 默认开启 build.modulePreload,Webpack 可配 webpack-preload-plugin,Rollup 推荐 rollup-plugin-module-preload。它们能静态分析 AST,提取所有静态 import 路径并注入完整列表。
最容易被忽略的是:modulepreload 只对静态 import 生效,动态 import() 和 import map 映射的 bare specifier 不在它的作用范围内;而且即使预加载成功,模块语法错误、路径拼错、CSP 拦截这些执行期问题,依然会导致 Failed to load module script,和预加载无关。