关于前端性能优化的一些问题:

46 阅读12分钟

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" 的作用

<link rel="preload" href="main.js" as="script">

as 属性告诉浏览器:

  1. 这是什么类型的资源 → 浏览器按对应优先级调度(脚本的优先级高于图片等)
  2. 是否遵循 CSP → 确保内容安全策略正确应用
  3. 是否复用已有缓存 → 同类型的缓存匹配更准确

为什么不用 <script> 标签直接加载?

<!-- 这样写会立即执行,可能阻塞渲染 -->
<script src="main.js"></script>

<!-- preload 只下载,不执行,等你真正需要时再引入 -->
<link rel="preload" href="main.js" as="script">
<script src="main.js" defer></script>

典型场景:

  • 你确定某个脚本稍后一定会用到(如路由懒加载的 chunk、Web Worker、后续交互需要的库)
  • 但不想让它现在执行阻塞首屏
  • 先偷偷下载好,放到内存里待命

为什么不用 prefetch

<!-- prefetch:优先级极低,适合下一页可能用到的资源 -->
<link rel="prefetch" href="next-page.js">

<!-- preload:当前页高优先级,必须用的资源 -->
<link rel="preload" href="main.js" as="script">

一句话总结: <link> 是浏览器资源加载体系的"标准接口",preload 利用这个接口实现了尽早发现、高优先级下载、但不执行的精确控制,这是任何其他方式都无法同时满足的。


2、使用场景:Vue 大屏项目,首屏需要加载 ECharts

你的项目结构可能是:

<!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>

三种方案的网络时间线对比

方案1: <script> 直接引入
─────────────────────────────────────────►
  │ 下载 │ 执行 │ 阻塞渲染 │  继续解析HTML │
  └─────┘     └─────────┘
  500KB JS 执行时,HTML 解析暂停,白屏时间长

方案2: <script defer>
─────────────────────────────────────────►
  │ HTML解析... │ 下载 │ 继续解析 │ 执行 │
  └─────────────┘      └─────────────────┘
  发现得晚(解析到 body 底部才看到),下载开始得晚

方案3: <link rel="preload"> + <script defer>
─────────────────────────────────────────►
  │ 下载 │ HTML解析... │ 继续解析 │ 执行 │
  └─────┘             └─────────────────┘
  在 <head> 就被发现,并行下载;HTML 解析完再执行

更实际的例子:路由懒加载的 chunk

假设你的 Vue 路由:

const Dashboard = () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue')

用户当前在首页,你预判他接下来很可能点"数据看板"。你可以在首页提前加载这个 chunk:

<!-- 首页 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 已经在内存里了,切换几乎是瞬时的。


一个反例:用错了会浪费

<!-- ❌ 错误:预加载了但没用 -->
<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 项目里要用一个很大的工具库

// 你的业务代码
import { debounce } from 'lodash-es';  // 或 import * as _ from 'lodash-es'

方案 1:什么都不做(默认行为)

<script type="module" src="/src/main.js"></script>

时间线:

解析 main.js → 遇到 import lodash-es → 发起请求 → 下载 → 解析执行
                                    ↑
                                    这里才开始下载,有延迟

方案 2:用 preload 提前下载

<head>
  <!-- 提前下载,但不执行 -->
  <link rel="preload" href="/node_modules/lodash-es/lodash.js" as="script">
</head>
<body>
  <script type="module" src="/src/main.js"></script>
</body>

时间线:

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

// Vite 会自动对动态 import 做 preload
const Dashboard = () => import('./views/Dashboard.vue')  // 自动生成 <link rel="modulepreload">

Vite 生成的是 <link rel="modulepreload">(专门用于 ESM),比普通的 preload 更高效,因为它会提前解析模块依赖图

手动控制的场景

只有当工具没帮你做、或者你想跨路由预加载时才需要手动写:

<!-- 用户在看列表页,你预判他会点详情 -->
<link rel="preload" href="/assets/DetailView-abc123.js" as="script">

关键区分:preload vs modulepreload

<!-- 普通 preload:只下载,不解析模块依赖 -->
<link rel="preload" href="/app.js" as="script">

<!-- modulepreload:下载 + 提前解析 ESM 依赖树(Vite 默认用这个) -->
<link rel="modulepreload" href="/app.js">
preloadmodulepreload
下载文件
解析 ESM 依赖
适用场景UMD / 非模块脚本ESM 模块

一句话总结

import * from 'xx' 里的 xx 不管是什么模块格式,文件本身都可以用 preload 提前下载。区别只在于:下载之后,是浏览器原生 ESM 解析,还是打包工具转换后再执行——但这发生在"下载完成"之后,不影响 preload 的作用。

4、Preload 与 Prefetch 的区别是什么?

直接对比,用你熟悉的场景来理解。


核心区别

PreloadPrefetch
目的当前页面必需的资源下一页可能用到的资源
优先级高(与关键资源同级)低(浏览器空闲时才下载)
时机立即下载,不等待等当前页面加载完、CPU/网络空闲时
缓存策略当前页会话可能进入 HTTP 缓存,供后续页面用
用错了的后果浪费带宽,阻塞关键资源基本无害,只是可能白下载

具体场景对比

场景:你的 Vue 管理后台

当前页:用户列表页
下一页可能去:用户详情页、角色管理页
<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>

浏览器行为:

页面加载开始
  │
  ▼
┌─────────────────┐     ┌─────────────────┐
│  高优先级队列    │     │  低优先级队列    │
│  (preload)      │     │  (prefetch)     │
│                 │     │                 │
│  user-table.js  │◄────│  role-manage.js │
│  user-table.css │     │  user-detail.js │
│                 │     │                 │
│  立即并行下载    │     │  等空闲了再下载  │
└─────────────────┘     └─────────────────┘

一句话记忆

Preload = "我现在就要,马上送到"(外卖加急)
Prefetch = "等你有空顺便带过来"(顺路快递)


常见误用

<!-- ❌ 错误:用 preload 加载下一页资源 -->
<link rel="preload" href="/next-page.js" as="script">
<!-- 结果:抢占当前页带宽,首屏变慢 -->

<!-- ❌ 错误:用 prefetch 加载当前必需资源 -->
<link rel="prefetch" href="/critical-component.js">
<!-- 结果:优先级太低,可能迟迟不下载,页面卡顿 -->

现代工具中的体现

工具自动行为
Webpackimport(/* webpackPrefetch: true */ './modal') 生成 prefetch
Vite动态 import 自动 modulepreload;prefetch 需手动配置
Next.js<Link prefetch> 自动预取目标页面资源

总结表

问题答案
当前路由的 chunk 懒加载?preload / modulepreload
其他路由可能用到的 chunk?prefetch
字体文件(首屏必需)?preload
下一页的背景图?prefetch
不确定用哪个?宁可 prefetch 也别乱用 preload

5、更详细的解释:

好的,深入到底层机制和实际工程细节。


一、浏览器资源优先级体系

浏览器对每种资源有固定的优先级权重,preloadprefetch 的核心差异就体现在这里。

优先级从高到低:

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 的请求头

GET /main.js HTTP/2
:authority: example.com
:method: GET
:path: /main.js
:scheme: https

Priority: u=0, i  ← 这里 u=0 表示最高优先级(HTTP/2 优先级树)

Prefetch 的请求头

GET /next-page.js HTTP/2
Priority: u=5, i  ← u=5 表示最低优先级

在 HTTP/2 中,浏览器用优先级树管理多路复用的流。preload 的流会被服务器优先发送,prefetch 的流可能被推迟到所有高优先级流传输完毕后。


三、缓存位置的差异

浏览器缓存层级:

┌─────────────────┐
│   Memory Cache  │  ← preload 加载后通常先放这里(快,页面关闭即失效)
│   (内存缓存)   │
├─────────────────┤
│   Disk Cache    │  ← prefetch 通常直接放这里(慢,但持久)
│   (磁盘缓存)   │
├─────────────────┤
│   Push Cache    │  ← HTTP/2 Server Push 专用(会话期内)
│   (推送缓存)   │
├─────────────────┤
│   Service Worker│  ← 离线缓存
├─────────────────┤
│   HTTP Cache    │  ← 标准 HTTP 缓存(max-age 等)
└─────────────────┘
特性PreloadPrefetch
默认缓存位置Memory CacheDisk Cache
页面刷新后需重新 preload(内存清空)可能命中 Disk Cache
跨页面共享❌ 不共享✅ 可共享(同域名)
容量限制小(内存有限)大(磁盘空间大)

四、实际网络抓包对比

假设页面同时有 preloadprefetch

时间轴(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)有两条队列:

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/2HTTP/3

prefetch 的特殊处理

  • Chrome 会在 onload 事件触发后,才正式把 prefetch 请求放入网络队列
  • 如果用户切换页面,prefetch 请求可能被取消
  • 移动端/省电模式下,prefetch 可能被完全忽略

六、内存占用与性能开销

Preload 的隐藏成本

<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-storeprefetch 不会缓存

七、与打包工具的深度集成

Webpack 的魔法注释

// 当前页必需:preload
import(
  /* webpackChunkName: "chart" */
  /* webpackPreload: true */
  './HeavyChart.vue'
);

// 可能用到:prefetch
import(
  /* webpackChunkName: "modal" */
  /* webpackPrefetch: true */
  './ConfirmModal.vue'
);

Webpack 生成的 HTML:

<!-- 插入到 <head> -->
<link rel="preload" href="/chart.js" as="script">

<!-- 插入到 </body> 前 -->
<link rel="prefetch" href="/modal.js">

Vite 的差异

Vite 对 ESM 更激进:

// 动态导入,Vite 自动 modulepreload
const mod = await import('./big-module.js');

生成的 HTML:

<link rel="modulepreload" href="/big-module.js" />

modulepreloadpreload 多一步:提前解析模块的 import 依赖树,构建完整的模块图谱,等执行时直接实例化。


八、真实项目的决策流程图

开始:需要预加载某个资源?
        
        
    当前页必需? ──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,或减少同域资源

十、终极对比表

维度PreloadPrefetchModulepreload
标准HTML5HTML5HTML5(提案)
优先级Very HighLowHigh
下载时机立即空闲时立即
解析依赖
缓存位置MemoryDiskMemory
跨页面
取消机制页面卸载时随时可取消页面卸载时
适用格式任意任意ESM only
打包工具Webpack/Vite 支持Webpack/Vite 支持Vite 默认
误用代价高(阻塞首屏)低(浪费一点带宽)

一句话总结

preload加急快递(现在就要,影响当前体验),prefetch顺路捎带(晚点也行,不着急)。用错 preload 会拖慢首屏,用错 prefetch 顶多白下载——所以不确定时,宁可保守用 prefetch