# 理解 preload 与 modulepreload：资源预加载完全指南

- **HTML:** <https://blog.asuk.app/zh/p/modulepreload-explained>
- **Language:** `zh`
- **Author:** Asuka
- **Published:** `2026-01-07T00:00:00.000Z`
- **Updated:** `2026-10-13T10:42:06.000Z`
- **Tags:** Web Performance, JavaScript, ES Modules

> 深入理解 preload 的使用场景与最佳实践，以及 modulepreload 如何解决 ES 模块加载瀑布、CORS 凭据模式和构建工具资源注入问题。

## Content

浏览器在解析 HTML 时会按顺序发现资源。但很多关键资源并不会第一时间出现在 HTML 里：字体藏在 CSS 的 `@font-face` 中，LCP 图片可能是 CSS 背景图，JavaScript 模块的深层依赖要等入口模块下载和解析后才会被发现。

`<link rel="preload">` 和 `<link rel="modulepreload">` 都是在解决“资源发现太晚”的问题。区别在于：

- `preload` 是通用资源预加载机制，适用于字体、图片、CSS、fetch 等资源。
- `modulepreload` 是 ES Module 专用机制，除了下载模块文件，还会按模块语义处理和准备模块。

理解这两个标签的差异，可以避免两类常见问题：一类是该预加载的关键资源太晚发现，另一类是过度预加载导致首屏带宽被浪费。


## `<link rel="preload">`：通用资源预加载

`preload` 是一个声明式资源获取指令，意思是：这个资源当前页面很快会用到，请浏览器尽早开始请求。

常见写法：

```html
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/styles/critical.css" as="style">
<link rel="preload" href="/images/hero.webp" as="image" fetchpriority="high">
<link rel="preload" href="/api/bootstrap.json" as="fetch" crossorigin>
```

`as` 属性非常关键。它告诉浏览器资源类型，影响：

- 请求优先级。
- Accept header。
- 内容安全策略匹配。
- 缓存复用方式。
- 是否需要 CORS。

常见 `as` 值包括：

| `as` | 适合资源 | 典型场景 |
|---|---|---|
| `font` | woff2 / woff 字体 | 首屏会用到的 Web Font |
| `image` | 图片 | LCP hero 图、CSS 背景图 |
| `style` | CSS | 之后会被 stylesheet 使用的关键 CSS |
| `script` | classic script | 之后会被普通 script 使用的 JS |
| `fetch` | JSON / API / WASM 等 fetch 请求 | 首屏 bootstrap 数据 |
| `worker` | Worker 脚本 | 很快会启动的 Worker |

如果 `as` 写错，浏览器可能无法复用缓存，甚至发起重复请求。


## preload 的工作原理

浏览器看到 preload 后，一般会：

1. 立即发起资源请求。
2. 根据 `as`、`type`、`crossorigin`、`fetchpriority` 等信息决定优先级和请求模式。
3. 把响应放入可复用的缓存位置。
4. 等后续真正的消费者出现，例如 `<img>`、`@font-face`、`fetch()`、`<link rel="stylesheet">`。
5. 如果请求模式匹配，就复用刚才的结果。

关键点是：**preload 只负责提前获取，不负责实际使用。**

例如：

```html
<link rel="preload" href="/styles/critical.css" as="style">
```

这只会下载 CSS，不会把样式应用到页面。要应用样式，仍然需要：

```html
<link rel="stylesheet" href="/styles/critical.css">
```

同理，预加载脚本不会执行脚本；预加载图片不会显示图片；预加载 JSON 不会自动把数据交给你的应用。


## preload 的典型使用场景


### 1. 预加载首屏字体

字体通常在 CSS 中通过 `@font-face` 声明。浏览器要等 CSS 下载、解析，并确认页面上有文本实际需要这个字体后，才会请求字体文件。

如果这个字体影响首屏文本，可以显式 preload：

```html
<link
  rel="preload"
  href="/fonts/inter-var.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>
```

字体 preload 通常需要 `crossorigin`。即使字体文件和页面同源，也要和实际字体请求的 CORS 模式保持一致，否则可能重复下载。

不过不要把所有字体都 preload。一般只预加载首屏真正会用到的 woff2，避免把粗体、斜体、多语言大字库全部塞进关键路径。


### 2. 预加载 LCP 图片

如果 LCP 元素是一张很重要的 hero 图，尤其是 CSS 背景图，浏览器可能发现得很晚。

可以写：

```html
<link
  rel="preload"
  href="/images/hero-banner.webp"
  as="image"
  fetchpriority="high"
>
```

如果是响应式图片，还可以配合 `imagesrcset` 和 `imagesizes`，让浏览器选择合适尺寸：

```html
<link
  rel="preload"
  as="image"
  href="/hero-1280.webp"
  imagesrcset="/hero-640.webp 640w, /hero-1280.webp 1280w"
  imagesizes="100vw"
  fetchpriority="high"
>
```

现代页面里，LCP 图片往往也可以直接在 `<img>` 上使用 `fetchpriority="high"`。如果图片本身很早就出现在 HTML 中，单独 preload 未必有明显收益。


### 3. 预加载首屏 bootstrap 数据

如果 SSR 页面 hydration 前必定要请求一份 JSON，可以考虑：

```html
<link rel="preload" href="/api/bootstrap.json" as="fetch" crossorigin>
```

然后应用里正常 fetch：

```ts
await fetch("/api/bootstrap.json", { credentials: "same-origin" });
```

这里要特别注意 `crossorigin` 和 `credentials` 是否一致。请求模式不一致时，浏览器可能不会复用 preload 结果。


## Fetch Priority 与 preload 的关系

`preload` 会让资源更早进入请求队列，但优先级还会受到资源类型、浏览器策略和 `fetchpriority` 影响。

例如：

```html
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<link rel="preload" href="/below-fold.webp" as="image" fetchpriority="low">
```

`fetchpriority` 是提示，不是强制命令。浏览器仍然会根据当前页面状态、连接情况、资源类型和调度策略做最终决定。

实践上：

- LCP 图片可以考虑 `fetchpriority="high"`。
- 非关键图片不应该 preload；确实要 preload 时也应考虑 `low`。
- 不要用 preload + high 把一堆非关键资源塞进首屏竞争队列。


## preload 的常见坑


### 1. 预加载后没有及时使用

Chrome 会对未及时使用的 preload 发出类似警告：

```text
The resource ... was preloaded using link preload but not used within a few seconds from the window's load event.
```

这通常说明资源并不属于当前页面关键路径，应该删除 preload 或改成 prefetch。


### 2. CORS / credentials 不匹配导致重复请求

preload 的请求模式必须和实际使用时一致。

常见重复请求来源：

- 字体 preload 忘记 `crossorigin`。
- `as="fetch"` 没有和后续 `fetch()` 的 credentials 模式匹配。
- script / module 的 `crossorigin` 不一致。


### 3. 预加载过多，反而拖慢首屏

preload 不是“越多越快”。它会让资源更早竞争带宽和连接。如果你把非关键资源都 preload，可能会挤占 CSS、字体、LCP 图片和主脚本。

一个实用原则是：只 preload 当前页面首屏必定会用、并且浏览器默认发现较晚的资源。


## ES Module 的加载问题

普通 `<script>` 通常是单个文件：浏览器下载、解析、执行。

ES Module 不一样。它天然形成依赖图：

```ts
// main.js
import { helper } from "./utils.js";
import { render } from "./renderer.js";
```

浏览器加载模块入口时，必须：

1. 下载入口模块。
2. 解析入口模块，发现静态 `import`。
3. 下载依赖模块。
4. 解析依赖模块，继续发现更深层依赖。
5. 等整个模块图完成后，再按模块规则执行。

如果依赖层级很深，就会出现模块发现瀑布：每一层都要等上一层下载和解析后才能发现。


## 为什么不应该用普通 preload 解决模块图问题

你当然可以写：

```html
<link rel="preload" href="/assets/main.js" as="script" crossorigin>
<script type="module" src="/assets/main.js" crossorigin></script>
```

这在某些情况下可能让入口文件更早下载。但它不是 ES Module 的专用机制，存在几个问题：

1. 它不会按照 module script 的语义准备模块记录。
2. 它不会帮你处理模块图里的静态依赖。
3. 它要求你非常小心地匹配 CORS / credentials 模式。
4. 它很容易只优化入口文件，却没有解决深层依赖瀑布。

因此，对于 ES Module，语义上更正确的做法是 `modulepreload`。


## `<link rel="modulepreload">`：ES Module 专用预加载

`modulepreload` 是为 module script 设计的预加载机制：

```html
<link rel="modulepreload" href="/assets/main.js">
<link rel="modulepreload" href="/assets/vendor.js">
<script type="module" src="/assets/main.js"></script>
```

它和普通 preload 的关键区别是：

- 资源按 module script 处理。
- 浏览器会获取、解析并编译模块。
- 模块会被放进模块映射中，后续 import 可以复用。
- 默认 credentials mode 是 `same-origin`。
- 可以通过 `crossorigin` 调整 credentials mode。
- 浏览器可以选择继续获取模块的依赖，但这在规范中是可选行为，不能假设所有浏览器都会递归预加载完整依赖图。

所以，最可靠的做法仍然是：构建工具或 SSR 框架显式输出当前页面需要的入口模块和关键依赖模块的 `modulepreload`。


## modulepreload 的 crossorigin 怎么理解

一个常见误解是：`modulepreload` 会“自动 CORS”，所以完全不用写 `crossorigin`。

更准确的说法是：module script 本身就按 CORS 规则加载，`modulepreload` 也遵循同一套 module script 相关的请求语义。

默认情况下，modulepreload 使用 `same-origin` credentials mode：

```html
<link rel="modulepreload" href="/assets/main.js">
<script type="module" src="/assets/main.js"></script>
```

这对同源资源通常没问题。

如果你跨域加载模块，或者你的 `<script type="module">` 使用了 `crossorigin`，就应该让两边保持一致：

```html
<link rel="modulepreload" href="https://cdn.example.com/main.js" crossorigin>
<script type="module" src="https://cdn.example.com/main.js" crossorigin></script>
```

如果凭据模式不一致，仍然可能导致重复请求或加载失败。


## modulepreload 能不能处理动态 import？

`modulepreload` 主要处理静态模块资源。动态 `import()` 是运行时行为，浏览器在解析入口模块时未必能知道用户之后会点哪个按钮、打开哪个面板。

如果你知道某个动态模块马上会用到，可以主动插入：

```ts
function preloadEditor() {
  const link = document.createElement("link");
  link.rel = "modulepreload";
  link.href = "/assets/editor.js";
  document.head.appendChild(link);
}

button.addEventListener("mouseenter", preloadEditor);
```

但如果只是“下一页可能会用到”，通常 `prefetch` 更合适，因为它优先级更低，不容易抢首屏资源。


## 性能对比

假设模块依赖结构是：

```text
main.js
├── utils.js
│   └── helpers.js
└── renderer.js
    └── dom.js
```

没有预加载时，浏览器可能经历：

```text
T0: 请求 main.js
T1: main.js 下载并解析，发现 utils.js 和 renderer.js
T2: 请求 utils.js 和 renderer.js
T3: 依赖下载并解析，发现 helpers.js 和 dom.js
T4: 请求 helpers.js 和 dom.js
T5: 全部准备完成，开始执行
```

显式 modulepreload 关键模块后：

```html
<link rel="modulepreload" href="/assets/main.js">
<link rel="modulepreload" href="/assets/utils.js">
<link rel="modulepreload" href="/assets/helpers.js">
<link rel="modulepreload" href="/assets/renderer.js">
<link rel="modulepreload" href="/assets/dom.js">
<script type="module" src="/assets/main.js"></script>
```

浏览器可以更早并行获取和准备这些模块，减少模块发现瀑布。

真实项目里不会手写这么多 link。通常由 Vite、React Router、Next、Astro、SvelteKit 等构建工具或框架根据 manifest 自动生成。


## 构建工具中的 modulepreload

现代构建工具会自动插入或生成 modulepreload 信息。

Vite 的生产构建会对入口 HTML 注入 modulepreload，用于预加载入口 chunk 的直接依赖。它还提供 `build.modulePreload` 选项，用来控制 polyfill、依赖解析等行为。

典型输出类似：

```html
<link rel="modulepreload" crossorigin href="/assets/vendor.d4e5f6.js">
<script type="module" crossorigin src="/assets/main.a1b2c3.js"></script>
```

SSR 框架则通常会根据当前 route matches 和构建 manifest 输出当前页面所需的 modulepreload。

以 React Router Framework Mode 为例，构建阶段会从 Vite manifest 收集 route module 及其 imports；SSR 阶段由 `<Scripts />` 根据当前匹配路由输出对应的 `modulepreload`。


## `preload` vs `modulepreload` vs `prefetch`

| 特性 | `preload` | `modulepreload` | `prefetch` |
|---|---|---|---|
| 主要用途 | 当前页面很快要用的通用资源 | 当前页面很快要用的 ES Module | 未来导航或空闲时可能用的资源 |
| 优先级倾向 | 较高，取决于 `as` | 较高，按 module script 处理 | 较低，通常空闲时获取 |
| 是否执行 / 应用 | 否 | 否，只准备模块 | 否 |
| 是否按模块语义处理 | 否 | 是 | 否 |
| 是否适合字体 | 是 | 否 | 通常否 |
| 是否适合 ESM 入口和依赖 | 不推荐 | 是 | 未来可能用时可考虑 |
| CORS / credentials | 需要自己匹配实际消费者 | 按 module script 语义，也要注意一致性 | 需要注意复用条件 |

选择建议：

- 首屏字体、LCP 图片、关键 fetch 数据 → `preload`
- 当前页面 hydration 或入口所需 ES modules → `modulepreload`
- 下一页或未来交互可能用到的资源 → `prefetch`


## 浏览器支持

`preload` 和 `modulepreload` 都已被现代主流浏览器支持。对于不认识的 link relation，浏览器通常会安全忽略，不会导致页面报错。

不过“支持标签”和“所有细节行为完全一致”不是一回事。尤其是 modulepreload 是否递归获取依赖、资源优先级调度、fetchpriority 具体影响，都可能随浏览器版本和实现策略变化。

因此，性能优化不要只凭直觉：应该结合 DevTools Network、Performance、Lighthouse、WebPageTest 或真实用户监控数据验证。


## 实战检查清单

使用 preload / modulepreload 前，可以问自己几个问题：

1. 这个资源是不是当前页面首屏一定会用？
2. 浏览器默认发现它是不是太晚？
3. 它是否会和更关键的 CSS、字体、LCP 图片、主脚本争抢带宽？
4. `as`、`type`、`crossorigin`、`fetchpriority` 是否和真实使用方式匹配？
5. 是否有 Chrome “preloaded but not used” 警告？
6. 这个资源是否已经被框架或构建工具自动处理？
7. 对 ESM 来说，我 preload 的是当前页面需要的模块，还是未来页面才可能用的模块？


## 常见错误示例


### 错误 1：preload CSS 但忘记真正应用

```html
<link rel="preload" href="/app.css" as="style">
```

这不会应用样式。需要：

```html
<link rel="stylesheet" href="/app.css">
```

或者使用明确的 preload-onload 技巧，但那通常要谨慎评估兼容性和可维护性。


### 错误 2：字体 preload 漏掉 crossorigin

```html
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2">
```

更稳妥：

```html
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
```


### 错误 3：把未来页面资源都 modulepreload

```html
<link rel="modulepreload" href="/assets/settings-page.js">
<link rel="modulepreload" href="/assets/admin-page.js">
<link rel="modulepreload" href="/assets/editor-page.js">
```

如果这些页面不是当前页面马上要执行的 hydration 资源，就可能抢首屏带宽。更适合使用路由级 prefetch，或者等用户 hover / viewport 接近 / idle 时再加载。


## 小结

`preload` 和 `modulepreload` 都是在帮浏览器更早发现资源，但它们解决的是不同问题。

`preload` 是通用工具，适合当前页面很快要用、但默认发现较晚的字体、LCP 图片、关键 CSS 或 fetch 数据。

`modulepreload` 是 ES Module 专用工具，适合当前页面很快要用的模块入口和依赖。它按 module script 语义获取、解析和准备模块，可以减少模块发现瀑布。

`prefetch` 则更适合未来可能访问的页面或低优先级资源。

真正的最佳实践不是“多加几个 preload”，而是用构建产物、Network waterfall 和 Web Vitals 去判断：哪个资源发现太晚，哪个资源真的阻塞首屏，然后只对这些资源下手。


## 参考资料

- [MDN: rel="preload"](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload)
- [MDN: rel="modulepreload"](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/modulepreload)
- [HTML Standard: Link types](https://html.spec.whatwg.org/multipage/links.html#linkTypes)
- [web.dev: Preload critical assets](https://web.dev/articles/preload-critical-assets)
- [Vite: build.modulePreload](https://vite.dev/config/build-options.html#build-modulepreload)
