1、<link rel="preload" href="main.js"> 作为预加载,为何使用link呢?为什么用 <link rel="preload"> 而不是其他方式?
这是一个关于浏览器资源加载机制的问题。
为什么用 <link rel="preload"> 而不是其他方式
核心原因:<link> 是浏览器原生支持的资源提示机制,能在 HTML 解析早期就被发现和处理。
具体解释
| 特性 | <link rel="preload"> |
其他方式(如 JS 动态创建) |
|---|---|---|
| 发现时机 | HTML 解析阶段即可发现,无需等待 JS 执行 | 必须等 JS 下载、解析、执行后才能发起 |
| 优先级 | 浏览器赋予较高网络优先级,可指定 as 类型 |
普通脚本加载,优先级由浏览器默认决定 |
| 缓存行为 | 加载后进入内存缓存,后续引用直接使用 | 可能重复下载或缓存策略不一致 |
| 不阻塞渲染 | 只下载,不执行,不阻塞 HTML 解析 | JS 执行可能阻塞主线程 |
as="script" 的作用
html
<link rel="preload" href="main.js" as="script">
as 属性告诉浏览器:
- 这是什么类型的资源 → 浏览器按对应优先级调度(脚本的优先级高于图片等)
- 是否遵循 CSP → 确保内容安全策略正确应用
- 是否复用已有缓存 → 同类型的缓存匹配更准确
为什么不用 <script> 标签直接加载?
html
<!-- 这样写会立即执行,可能阻塞渲染 -->
<script src="main.js"></script>
<!-- preload 只下载,不执行,等你真正需要时再引入 -->
<link rel="preload" href="main.js" as="script">
<script src="main.js" defer></script>
典型场景:
- 你确定某个脚本稍后一定会用到(如路由懒加载的 chunk、Web Worker、后续交互需要的库)
- 但不想让它现在执行阻塞首屏
- 先偷偷下载好,放到内存里待命
为什么不用 prefetch?
html
<!-- prefetch:优先级极低,适合下一页可能用到的资源 -->
<link rel="prefetch" href="next-page.js">
<!-- preload:当前页高优先级,必须用的资源 -->
<link rel="preload" href="main.js" as="script">
一句话总结: <link> 是浏览器资源加载体系的"标准接口",preload 利用这个接口实现了尽早发现、高优先级下载、但不执行的精确控制,这是任何其他方式都无法同时满足的。
2、使用场景:Vue 大屏项目,首屏需要加载 ECharts
你的项目结构可能是:
html
<!DOCTYPE html>
<html>
<head>
<title>数据可视化大屏</title>
<!-- 方案1:普通 script 引入(阻塞渲染) -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<!-- 方案2:defer 引入(发现晚,下载可能不及时) -->
<script src="echarts.min.js" defer></script>
<!-- 方案3:preload + defer(最优) -->
<link rel="preload" href="echarts.min.js" as="script">
<script src="echarts.min.js" defer></script>
</head>
<body>
<div id="app"></div>
<script src="app.js"></script>
</body>
</html>
三种方案的网络时间线对比
xml
方案1: <script> 直接引入
─────────────────────────────────────────►
│ 下载 │ 执行 │ 阻塞渲染 │ 继续解析HTML │
└─────┘ └─────────┘
500KB JS 执行时,HTML 解析暂停,白屏时间长
方案2: <script defer>
─────────────────────────────────────────►
│ HTML解析... │ 下载 │ 继续解析 │ 执行 │
└─────────────┘ └─────────────────┘
发现得晚(解析到 body 底部才看到),下载开始得晚
方案3: <link rel="preload"> + <script defer>
─────────────────────────────────────────►
│ 下载 │ HTML解析... │ 继续解析 │ 执行 │
└─────┘ └─────────────────┘
在 <head> 就被发现,并行下载;HTML 解析完再执行
更实际的例子:路由懒加载的 chunk
假设你的 Vue 路由:
js
const Dashboard = () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue')
用户当前在首页,你预判他接下来很可能点"数据看板"。你可以在首页提前加载这个 chunk:
html
<!-- 首页 HTML -->
<head>
<!-- 提前下载 dashboard 的 JS 和 CSS,但不执行 -->
<link rel="preload" href="/js/dashboard.abc123.js" as="script">
<link rel="preload" href="/css/dashboard.abc123.css" as="style">
</head>
当用户点击路由时,chunk 已经在内存里了,切换几乎是瞬时的。
一个反例:用错了会浪费
html
<!-- ❌ 错误:预加载了但没用 -->
<link rel="preload" href="some-lib.js" as="script">
<!-- 页面从头到尾没有引用 some-lib.js -->
<!-- 结果:白白浪费带宽和内存,控制台还会警告 -->
<!-- ❌ 错误:as 类型写错 -->
<link rel="preload" href="image.png" as="script">
<!-- 浏览器按脚本优先级下载,但实际是图片,缓存不匹配,可能重复下载 -->
<!-- ✅ 正确 -->
<link rel="preload" href="image.png" as="image">
一句话记忆
preload就像你点外卖时,提前让骑手把餐送到楼下大堂(下载好),但你说"先别送上来,等我电话"(不执行)。等你真正需要时,一个电话 30 秒就送到门口(从内存缓存直接执行),而不是临时下单等 30 分钟。
3、问题:ESmodule是否适用?
ES Module 的 import 同样适用,但需要理解一个关键区别。
核心结论
<link rel="preload"> 预加载的是"文件",不是"模块依赖关系"。
它只管提前把文件内容下载到浏览器缓存 ,至于文件内部是 UMD、ESM 还是 CommonJS 格式,浏览器不关心。真正解析和执行模块的是后续的 import 或 <script type="module">。
具体例子
场景:你的 Vue 项目里要用一个很大的工具库
js
// 你的业务代码
import { debounce } from 'lodash-es'; // 或 import * as _ from 'lodash-es'
方案 1:什么都不做(默认行为)
html
<script type="module" src="/src/main.js"></script>
时间线:
arduino
解析 main.js → 遇到 import lodash-es → 发起请求 → 下载 → 解析执行
↑
这里才开始下载,有延迟
方案 2:用 preload 提前下载
html
<head>
<!-- 提前下载,但不执行 -->
<link rel="preload" href="/node_modules/lodash-es/lodash.js" as="script">
</head>
<body>
<script type="module" src="/src/main.js"></script>
</body>
时间线:
arduino
preload 发现 → 立即下载 lodash.js ──┐
├──→ 等 main.js 里 import 时,直接从缓存取
main.js 解析 → import lodash-es ────┘
UMD vs ESM 有区别吗?
| 格式 | preload 是否适用 | 注意事项 |
|---|---|---|
UMD (如 lodash.min.js) |
✅ 完全适用 | 直接 <script src> 或 import 都能用 |
ESM (如 lodash-es) |
✅ 同样适用 | preload 下载的是原始文件 ,后续 import 时浏览器会按 ESM 规范解析 |
| CommonJS(如 Node 环境) | ❌ 浏览器不支持 | 需要打包工具转换后才能用 |
实际开发中的写法
现代打包工具(Vite / Webpack / Rollup)
这些工具会自动处理,你通常不需要手动写 preload:
js
// Vite 会自动对动态 import 做 preload
const Dashboard = () => import('./views/Dashboard.vue') // 自动生成 <link rel="modulepreload">
Vite 生成的是 <link rel="modulepreload">(专门用于 ESM),比普通的 preload 更高效,因为它会提前解析模块依赖图。
手动控制的场景
只有当工具没帮你做、或者你想跨路由预加载时才需要手动写:
html
<!-- 用户在看列表页,你预判他会点详情 -->
<link rel="preload" href="/assets/DetailView-abc123.js" as="script">
关键区分:preload vs modulepreload
html
<!-- 普通 preload:只下载,不解析模块依赖 -->
<link rel="preload" href="/app.js" as="script">
<!-- modulepreload:下载 + 提前解析 ESM 依赖树(Vite 默认用这个) -->
<link rel="modulepreload" href="/app.js">
preload |
modulepreload |
|
|---|---|---|
| 下载文件 | ✅ | ✅ |
| 解析 ESM 依赖 | ❌ | ✅ |
| 适用场景 | UMD / 非模块脚本 | ESM 模块 |
一句话总结
import * from 'xx'里的xx不管是什么模块格式,文件本身 都可以用preload提前下载。区别只在于:下载之后,是浏览器原生 ESM 解析,还是打包工具转换后再执行------但这发生在"下载完成"之后,不影响 preload 的作用。
4、Preload 与 Prefetch 的区别是什么?
直接对比,用你熟悉的场景来理解。
核心区别
| Preload | Prefetch | |
|---|---|---|
| 目的 | 当前页面必需的资源 | 下一页可能用到的资源 |
| 优先级 | 高(与关键资源同级) | 低(浏览器空闲时才下载) |
| 时机 | 立即下载,不等待 | 等当前页面加载完、CPU/网络空闲时 |
| 缓存策略 | 当前页会话 | 可能进入 HTTP 缓存,供后续页面用 |
| 用错了的后果 | 浪费带宽,阻塞关键资源 | 基本无害,只是可能白下载 |
具体场景对比
场景:你的 Vue 管理后台
当前页:用户列表页
下一页可能去:用户详情页、角色管理页
html
<head>
<!-- ✅ Preload:当前页渲染必须的 -->
<link rel="preload" href="/js/user-table-chunk.js" as="script">
<link rel="preload" href="/css/user-table.css" as="style">
<!-- ✅ Prefetch:用户可能点"角色管理",提前准备 -->
<link rel="prefetch" href="/js/role-manage-chunk.js">
<link rel="prefetch" href="/js/user-detail-chunk.js">
</head>
浏览器行为:
lua
页面加载开始
│
▼
┌─────────────────┐ ┌─────────────────┐
│ 高优先级队列 │ │ 低优先级队列 │
│ (preload) │ │ (prefetch) │
│ │ │ │
│ user-table.js │◄────│ role-manage.js │
│ user-table.css │ │ user-detail.js │
│ │ │ │
│ 立即并行下载 │ │ 等空闲了再下载 │
└─────────────────┘ └─────────────────┘
一句话记忆
Preload = "我现在就要,马上送到"(外卖加急)
Prefetch = "等你有空顺便带过来"(顺路快递)
常见误用
html
<!-- ❌ 错误:用 preload 加载下一页资源 -->
<link rel="preload" href="/next-page.js" as="script">
<!-- 结果:抢占当前页带宽,首屏变慢 -->
<!-- ❌ 错误:用 prefetch 加载当前必需资源 -->
<link rel="prefetch" href="/critical-component.js">
<!-- 结果:优先级太低,可能迟迟不下载,页面卡顿 -->
现代工具中的体现
| 工具 | 自动行为 |
|---|---|
| Webpack | import(/* webpackPrefetch: true */ './modal') 生成 prefetch |
| Vite | 动态 import 自动 modulepreload;prefetch 需手动配置 |
| Next.js | <Link prefetch> 自动预取目标页面资源 |
总结表
| 问题 | 答案 |
|---|---|
| 当前路由的 chunk 懒加载? | preload / modulepreload |
| 其他路由可能用到的 chunk? | prefetch |
| 字体文件(首屏必需)? | preload |
| 下一页的背景图? | prefetch |
| 不确定用哪个? | 宁可 prefetch 也别乱用 preload |
5、更详细的解释:
好的,深入到底层机制和实际工程细节。
一、浏览器资源优先级体系
浏览器对每种资源有固定的优先级权重,preload 和 prefetch 的核心差异就体现在这里。
ini
优先级从高到低:
Very High(最高)
├── HTML 文档本身
├── CSS(阻塞渲染的)
└── <link rel="preload"> 的资源(as="script/style/font")
High(高)
├── 首屏可见图片
├── @font-face 引用的字体(FOIT 阶段)
└── Fetch/XHR(同步请求)
Medium(中)
├── <script async>
├── <script defer>
└── <link rel="modulepreload">
Low(低)
├── 首屏外的图片
├── <link rel="prefetch">
└── 预解析 DNS(<link rel="dns-prefetch">)
Very Low(最低)
└── <link rel="prefetch"> 在弱网/省电模式下可能被完全跳过
关键结论 :preload 插队到最前面,prefetch 排队到最后面。
二、HTTP 请求层面的差异
Preload 的请求头
makefile
GET /main.js HTTP/2
:authority: example.com
:method: GET
:path: /main.js
:scheme: https
Priority: u=0, i ← 这里 u=0 表示最高优先级(HTTP/2 优先级树)
Prefetch 的请求头
vbnet
GET /next-page.js HTTP/2
Priority: u=5, i ← u=5 表示最低优先级
在 HTTP/2 中,浏览器用优先级树 管理多路复用的流。preload 的流会被服务器优先发送,prefetch 的流可能被推迟到所有高优先级流传输完毕后。
三、缓存位置的差异
arduino
浏览器缓存层级:
┌─────────────────┐
│ Memory Cache │ ← preload 加载后通常先放这里(快,页面关闭即失效)
│ (内存缓存) │
├─────────────────┤
│ Disk Cache │ ← prefetch 通常直接放这里(慢,但持久)
│ (磁盘缓存) │
├─────────────────┤
│ Push Cache │ ← HTTP/2 Server Push 专用(会话期内)
│ (推送缓存) │
├─────────────────┤
│ Service Worker│ ← 离线缓存
├─────────────────┤
│ HTTP Cache │ ← 标准 HTTP 缓存(max-age 等)
└─────────────────┘
| 特性 | Preload | Prefetch |
|---|---|---|
| 默认缓存位置 | Memory Cache | Disk Cache |
| 页面刷新后 | 需重新 preload(内存清空) | 可能命中 Disk Cache |
| 跨页面共享 | ❌ 不共享 | ✅ 可共享(同域名) |
| 容量限制 | 小(内存有限) | 大(磁盘空间大) |
四、实际网络抓包对比
假设页面同时有 preload 和 prefetch:
scss
时间轴(Chrome DevTools Network 面板):
0ms 100ms 200ms 300ms 400ms 500ms
│ │ │ │ │ │
├───────┤ │ │ │ │
│ preload.js │ │ │ │ ← 0ms 开始,100ms 完成(高优先级)
│ (100KB) │ │ │ │
├───────────────┤ │ │ │
│ CSS 渲染阻塞 │ │ │ │
│ (150KB) │ │ │ │
│ │ │ │ │
│ ├───────┼───────┤ │ │
│ │ prefetch.js │ │ │ ← 200ms 才开始(低优先级)
│ │ (200KB) │ │ │ 因为前面高优先级任务占满带宽
│ │ │ │ │
│ │ ├───────┼───────┤ │
│ │ │ 图片A │ │ │
│ │ │ (首屏) │ │ │
│ │ │ │ │ │
│ │ │ ├───────┼───────┤
│ │ │ │ prefetch 完成 │ ← 400ms 完成
│ │ │ │ │
关键观察 :prefetch 不是"慢",而是被故意推迟 。如果当前页资源很少、带宽空闲,prefetch 也可能很快完成。
五、Chrome 内部的实现细节
Chrome 的资源加载器(ResourceLoader)有两条队列:
less
ResourceLoader 内部结构:
┌─────────────────────────────────────────┐
│ Pending Request Queue │
│ (待处理请求队列,按优先级排序) │
├─────────────────────────────────────────┤
│ [P1] preload.js Priority: Very │
│ [P2] critical.css Priority: Very │
│ [P3] main.js Priority: High │
│ [P4] logo.png Priority: High │
│ ... │
│ [Pn] prefetch.js Priority: Low │ ← 排到末尾
│ [Pn+1] next-page.css Priority: Low │
└─────────────────────────────────────────┘
│
▼
网络层(HTTP/2 或 HTTP/3)
prefetch 的特殊处理:
- Chrome 会在
onload事件触发后,才正式把prefetch请求放入网络队列 - 如果用户切换页面,
prefetch请求可能被取消 - 移动端/省电模式下,
prefetch可能被完全忽略
六、内存占用与性能开销
Preload 的隐藏成本
html
<link rel="preload" href="/ huge-video.mp4" as="video">
问题:
- 视频文件被下载到 Memory Cache
- 如果页面最终没用这个视频(比如条件渲染没触发),内存不会自动释放
- Chrome 控制台会警告:
The resource was preloaded using link preload but not used within a few seconds
Chrome 的清理策略:
preload资源如果在 3 秒内没有被引用,会被标记为废弃- 但不会立即释放内存,只是降低优先级,等待 GC
Prefetch 的磁盘成本
- 默认写入 Disk Cache,占用磁盘空间
- 遵循 HTTP 缓存头(
Cache-Control: max-age=xxx) - 如果服务器返回
no-store,prefetch不会缓存
七、与打包工具的深度集成
Webpack 的魔法注释
js
// 当前页必需:preload
import(
/* webpackChunkName: "chart" */
/* webpackPreload: true */
'./HeavyChart.vue'
);
// 可能用到:prefetch
import(
/* webpackChunkName: "modal" */
/* webpackPrefetch: true */
'./ConfirmModal.vue'
);
Webpack 生成的 HTML:
html
<!-- 插入到 <head> -->
<link rel="preload" href="/chart.js" as="script">
<!-- 插入到 </body> 前 -->
<link rel="prefetch" href="/modal.js">
Vite 的差异
Vite 对 ESM 更激进:
js
// 动态导入,Vite 自动 modulepreload
const mod = await import('./big-module.js');
生成的 HTML:
html
<link rel="modulepreload" href="/big-module.js" />
modulepreload 比 preload 多一步:提前解析模块的 import 依赖树,构建完整的模块图谱,等执行时直接实例化。
八、真实项目的决策流程图
yaml
开始:需要预加载某个资源?
│
▼
当前页必需? ──Yes──► 用 <link rel="preload">
│ │
No ▼
│ 指定 as 属性了吗?
▼ │
下一页可能用? No ──► 浏览器无法确定优先级,preload 失效
│ │
Yes No Yes
│ │ ▼
▼ ▼ 检查控制台警告
prefetch 不用 "was preloaded but not used"
或不用 ──Yes──► 资源没用上,浪费带宽
│ │
│ No
│ ▼
│ 正常生效
▼
弱网环境? ──Yes──► 考虑不用 prefetch(可能被跳过)
│
No
▼
资源体积 > 2MB? ──Yes──► 谨慎使用,可能阻塞其他资源
│
No
▼
放心使用 prefetch
九、常见坑与解决方案
| 坑 | 原因 | 解决 |
|---|---|---|
| preload 了但没用到,控制台警告 | 条件渲染没触发,或路径写错 | 确保资源一定会被引用;或用 JS 动态创建 link |
| preload 字体但页面仍闪一下(FOUT) | crossorigin 属性缺失 |
<link rel="preload" href="font.woff2" as="font" crossorigin> |
| prefetch 跨域资源失败 | CORS 预检问题 | 加 crossorigin 或确保服务器允许 |
| 移动端 prefetch 不生效 | 省电模式/数据节省模式 | 这是预期行为,不要依赖 prefetch |
| HTTP/1.1 下 preload 反而慢 | 浏览器并发连接数限制(6个/域名) | 升级到 HTTP/2,或减少同域资源 |
十、终极对比表
| 维度 | Preload | Prefetch | Modulepreload |
|---|---|---|---|
| 标准 | HTML5 | HTML5 | HTML5(提案) |
| 优先级 | Very High | Low | High |
| 下载时机 | 立即 | 空闲时 | 立即 |
| 解析依赖 | ❌ | ❌ | ✅ |
| 缓存位置 | Memory | Disk | Memory |
| 跨页面 | ❌ | ✅ | ❌ |
| 取消机制 | 页面卸载时 | 随时可取消 | 页面卸载时 |
| 适用格式 | 任意 | 任意 | ESM only |
| 打包工具 | Webpack/Vite 支持 | Webpack/Vite 支持 | Vite 默认 |
| 误用代价 | 高(阻塞首屏) | 低(浪费一点带宽) | 中 |
一句话总结
preload是加急快递 (现在就要,影响当前体验),prefetch是顺路捎带 (晚点也行,不着急)。用错preload会拖慢首屏,用错prefetch顶多白下载------所以不确定时,宁可保守用prefetch。