SPA主Bundle必须用rel="preload"而非script标签,因其可在HTML解析阶段并行下载、抢占网络空闲窗口,提前400–800ms起始下载;而script标签会阻塞解析且无法控制优先级,且必须配as="script"、路径一致、避免重复请求。

在大型单页应用(SPA)中,对首屏关键 Bundle 使用 rel="preload" 能显著缩短首次交互时间,但前提是它被正确注入、类型匹配且不破坏模块加载链——否则反而引发重复请求或执行延迟。
为什么 SPA 的主 Bundle 必须用 preload 而不是 script 标签直接引入
现代 SPA(如 React/Vue 项目)常将首屏逻辑打包为一个 main.js 或 app.[hash].js,并依赖动态 import() 或 type="module" 启动。问题在于:浏览器只有解析完 HTML、遇到 <script type="module"> 或执行到 import('./app.js') 时才发起请求,此时已错过网络空闲窗口。
而 rel="preload" 可在 <head> 解析阶段就触发下载,与 HTML 解析并行。实测在 3G 网络下,main.js 下载起始时间可提前 400–800ms。
- 必须放在
<head>最早位置,早于任何<script>标签 - 不能用
<script src="main.js">替代——那会阻塞 HTML 解析,且无法控制优先级 - 若使用 Webpack/Vite,需确保构建后路径稳定(避免 hash 变动导致 preload 缓存失效)
as="script" 是硬性要求,漏写等于没写
as 属性决定浏览器是否把它当高优脚本处理。不写或写错,Chrome Network 面板里 Priority 显示 Low,和普通 fetch 无异;更糟的是,后续 <script src="main.js"> 仍会重新发起请求。
立即学习“前端免费学习笔记(深入)”;
- 必须写
as="script",不能是as="fetch"、as="module"或留空 - 如果主 Bundle 后续通过
type="module"加载,且含integrity,则 preload 也必须带相同integrity值,否则缓存不复用 - 服务端返回的 MIME 类型必须是
application/javascript,否则部分浏览器拒绝执行
和 modulepreload 的关键区别:别把它们混用
如果你的 SPA 主入口是 ES 模块(type="module"),且内部有静态 import 链(比如 import { render } from './core.js'),那么 rel="preload" 只下载 main.js 本身,子模块仍要等解析后才请求——这会导致多轮 RTT 延迟。
此时应改用 rel="modulepreload":
- 它会预解析
main.js并递归预取所有静态import的依赖(如core.js、router.js) - 不能加
as属性,加了反而被忽略 - href 必须和最终
import()或type="module"中写的路径完全一致(包括查询参数,如?v=1.2) - 多个 modulepreload 标签之间无顺序依赖,但必须全部出现在首个模块脚本之前
容易被忽略的缓存与执行时机问题
preload 只下载不执行,所以即使资源已就绪,也要靠后续 <script src="main.js"> 或 import() 触发执行。这个“接力”环节极易出错:
- 不要在 preload 后又写一个同 href 的
<script src="main.js">—— 若路径写法不一致(如一个带/开头、一个相对路径),浏览器视为不同资源,重复下载 - 如果使用
import()动态加载,确保该调用发生在 preload 资源已进入缓存之后(通常 DOMContentLoaded 后即可,无需额外等待) - 验证是否生效:打开 Chrome DevTools → Network → 筛选
Initiator: preload,检查Priority是否为Highest或High,再对比未加前的Start Time
真正起效的 preload 不是加在 HTML 里就完事,而是要和构建产物路径、模块加载方式、缓存策略严丝合缝地咬合——差一个斜杠、少一个 integrity、错一个 as,都可能让优化变成负优化。



















