spa首屏主bundle必须用rel="preload"而非script标签,因其在解析阶段即并行下载,可提前400–800ms起始请求、抢占网络空闲窗口;而script需等html解析到该标签才发起,且阻塞解析、无法提升优先级。

大型单页应用(SPA)的首屏主 Bundle 必须用 rel="preload",不能用 <script src="main.js"></script> 直接引入——否则下载起始时间晚 400–800ms,且无法抢占网络空闲窗口。
为什么 SPA 主 Bundle 一定要用 preload 而不是 script 标签
浏览器只有解析到 <script type="module"></script> 或执行到 import('./app.js') 才发起请求,此时 HTML 解析已过半,带宽可能被其他资源占用。而 <link rel="preload" href="main.js" as="script"> 在 解析阶段就触发下载,与 HTML 解析并行。
- 实测在 3G 网络下,
main.js下载起始时间可提前 400–800ms - 必须放在
最早位置,紧贴<meta charset>后、<title></title>前 - 若用
<script src="main.js"></script>替代,会阻塞 HTML 解析,且浏览器无法提升其网络优先级
as="script" 是硬性要求,漏写等于没写
as 属性决定浏览器是否把它当高优脚本处理。不写或写错,Chrome Network 面板里 Priority 显示为 Low,和普通 fetch() 无异;更糟的是,后续 <script src="main.js"></script> 仍会重新发起请求。
- 必须写
as="script",不能是as="fetch"、as="module"或留空 - 如果主 Bundle 后续通过
type="module"加载,且带integrity,则<link>也必须带完全相同的integrity值,否则缓存不复用 - 服务端返回的 MIME 类型必须是
application/javascript,否则 Safari/Edge 可能拒绝执行
modulepreload 才能递归预取静态 import 依赖
如果你的主入口是 ES 模块(type="module"),且内部有 import { render } from './core.js' 这类静态导入,只用 rel="preload" 是不够的:它只下载 main.js,子模块仍要等解析后才请求,多轮 RTT 延迟明显。
- 改用
<link rel="modulepreload" href="main.js">,它会预解析main.js并递归预取所有静态import的依赖 -
modulepreload不允许加as属性,加了会被忽略 -
href必须和import()或type="module"中写的路径完全一致(包括查询参数,如?v=1.2) - 多个
modulepreload标签之间无顺序依赖,但必须全部出现在首个模块脚本之前
构建产物带 hash 会导致 preload 失效
Webpack/Vite 默认给 JS 文件加 hash(如 app.a1b2c3.js),每次构建路径都变,导致 rel="preload" 缓存失效、重复下载,甚至指向 404。
- 对首屏关键 Bundle,建议禁用 hash 或固定文件名(如
main.js) - 若用 Vite,可通过
build.rollupOptions.output.entryFileNames控制输出名;Webpack 可配output.filename+HtmlWebpackPlugin动态注入真实路径 - 避免在 HTML 中硬编码带 hash 的路径——上线后
href指向 404,preload 彻底失效
最容易被忽略的点是:preload 只对当前页有效,且必须路径、MIME、integrity、crossorigin 全部对齐才能复用缓存;稍有偏差,Network 面板里 Initiator 就显示为 script 而非 preload,说明它根本没起作用。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











