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

エッセイ

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

Asuka 著

浏览器在解析 HTML 时会按顺序发现资源。但很多关键资源(字体、CSS、脚本)隐藏在 CSS 或 JavaScript 中,等浏览器发现它们时已经太晚了。<link rel="preload" /><link rel="modulepreload" /> 提供了一种机制,让开发者可以提前告知浏览器哪些资源是重要的。

preload1 是一个声明式的资源获取指令,告诉浏览器"这个资源很快就会用到,请尽早开始下载"。

基本语法#

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="/scripts/analytics.js" as="script">
<link rel="preload" href="/images/hero.webp" as="image">

as 属性是必须的2,它告诉浏览器资源的类型,影响请求优先级和缓存策略。常用值包括:

as 资源类型 示例
script JavaScript 文件 分析脚本、第三方库
style CSS 样式表 关键样式、字体样式
font 字体文件 woff2、woff
image 图片 hero 图、logo
fetch XHR/Fetch 请求 API 数据、JSON

典型使用场景#

预加载字体:字体通常在 CSS 中通过 @font-face 声明,浏览器要等到 CSSOM 构建完成且发现需要该字体的文本时才会请求。preload 可以提前触发下载:

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

注意字体预加载必须添加 crossorigin,即使是同源资源——因为字体请求总是使用 CORS。

预加载关键 CSS:对于通过 JavaScript 动态加载的样式,可以用 preload 提前获取:

HTML
<link rel="preload" href="/styles/above-fold.css" as="style">

预加载 LCP 图片3:如果最大内容绘制(LCP)元素是一张图片,尤其是 CSS 背景图,preload 可以显著提升 LCP 时间:

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

preload 的工作原理#

浏览器收到 preload 指令后会:

  1. 立即发起资源请求,不等待正常的资源发现流程。
  2. 将响应存入内存缓存。
  3. 等待资源被实际使用(如 <script> 标签引用、CSS @import 等)。
  4. 从缓存中提供资源,无需再次请求。

关键点:preload 只负责获取资源到缓存,不会执行脚本或应用样式。资源的实际使用由后续的标签触发。

Fetch Priority#

preload 请求的网络优先级由 as 属性决定,不同资源类型有默认优先级:

as 默认优先级
style Highest
font High
script High(或 Medium,取决于位置)
image Low(视口内为 High)

可以用 fetchpriority 属性在默认值基础上微调:

HTML
<!-- 提升 LCP 图片的优先级 -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">

<!-- 降低非关键脚本的优先级 -->
<link rel="preload" href="/analytics.js" as="script" fetchpriority="low">

preload 的注意事项#

需要在短时间内使用:如果预加载的资源在页面加载后短时间内没有被使用,Chrome 会在控制台发出警告。这表明预加载可能是不必要的,浪费了带宽。

Auto
The resource https://example.com/font.woff2 was preloaded using link preload but not used within a few seconds from the window's load event.

不要预加载过多资源:preload 让资源更早进入请求队列,可能与其他关键资源竞争带宽。只预加载真正影响首屏渲染的资源。

CORS 匹配:preload 请求的 CORS 模式必须与实际使用时一致,否则会导致重复请求。

ES 模块的加载问题#

传统 <script> 标签加载的脚本是独立的——浏览器下载、解析、执行,整个过程相对直接。但 ES 模块不同,它们形成依赖图谱:

TypeScript
// main.js
import { helper } from './utils.js';
import { render } from './renderer.js';
// renderer.js 又可能依赖其他模块...

浏览器加载 ES 模块时必须:

  1. 下载入口模块。
  2. 解析模块,发现 import 语句。
  3. 下载被导入的模块。
  4. 递归解析这些模块,发现更多依赖。
  5. 继续下载、解析,直到整个依赖图完成。

这就是所谓的模块解析瀑布(module resolution waterfall)——浏览器必须按顺序发现并加载依赖,无法提前知道完整的依赖图。对于深层嵌套的依赖,每一层都增加一次网络往返延迟。

为什么 ES 模块不能用 preload#

既然 preload 可以预加载脚本,为什么不用它来预加载 ES 模块?

preload 只负责下载:它把文件拉到浏览器缓存中,但不会进行模块特有的处理——解析模块语法、分析依赖图、准备模块执行环境。

模块解析仍然串行:即使文件已被 preload 缓存,当 <script type="module"> 执行时,浏览器仍需要从头开始模块解析流程——读取文件、分析 import、发现依赖、再请求依赖。瀑布效应并没有消除。

CORS 模式不匹配:ES 模块总是以 CORS 模式请求。如果 preload 没有正确设置 crossorigin 属性,浏览器会认为这是两个不同的请求,导致资源被重复下载:

HTML
<!-- 错误:CORS 模式不匹配,导致重复请求 -->
<link rel="preload" href="module.js" as="script">
<script type="module" src="module.js"></script>

<!-- 正确设置 crossorigin,但仍不理想 -->
<link rel="preload" href="module.js" as="script" crossorigin>
<script type="module" src="module.js"></script>

即使 CORS 设置正确,preload 也只能避免入口模块的重复下载,对于模块的依赖链无能为力。

modulepreload4 是专为 ES 模块设计的预加载机制:

HTML
<link rel="modulepreload" href="./utils.js">
<link rel="modulepreload" href="./renderer.js">
<script type="module" src="./main.js"></script>

完整的模块处理:不仅下载文件,还会解析模块、构建模块图、编译代码并准备好执行。当脚本真正需要这些模块时,它们已经处于可执行状态。

自动 CORS 处理modulepreload 默认使用与 ES 模块相同的 CORS 设置,无需手动添加 crossorigin

依赖预获取:浏览器可以选择性地预获取被 modulepreload 的模块的依赖(这是可选行为,具体取决于浏览器实现)。

仅限静态导入modulepreload 只解析静态 import 声明,不会处理动态 import()。动态导入是运行时行为,浏览器在解析阶段无法预知。如需预加载动态导入的模块,必须手动添加 <link rel="modulepreload">

性能对比#

考虑以下依赖结构:

Auto
main.js
├── utils.js
│   └── helpers.js
└── renderer.js
    └── dom.js

无预加载的情况

Auto
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="./main.js">
<link rel="modulepreload" href="./utils.js">
<link rel="modulepreload" href="./helpers.js">
<link rel="modulepreload" href="./renderer.js">
<link rel="modulepreload" href="./dom.js">
<script type="module" src="./main.js"></script>

所有模块并行下载和预处理,消除了瀑布效应。

实际应用#

构建工具集成#

现代构建工具会自动注入 modulepreload 标签。

Vite 默认为生产构建生成 modulepreload 链接:

HTML
<!-- Vite 生成的 HTML -->
<link rel="modulepreload" crossorigin href="/assets/main.a1b2c3.js">
<link rel="modulepreload" crossorigin href="/assets/vendor.d4e5f6.js">
<script type="module" crossorigin src="/assets/main.a1b2c3.js"></script>

Rollup 可以通过插件实现类似功能。

服务端渲染(SSR)#

SSR 框架通常在渲染时生成 modulepreload 标签:

TSX
// 服务端渲染时注入预加载
function renderHTML(manifest: ModuleInfo[]) {
  return `
    <head>
      ${manifest.map(m => `<link rel="modulepreload" href="${m.href}">`).join('\n')}
    </head>
  `;
}

动态导入的预加载#

对于使用 import() 动态导入的模块,可以在用户可能需要之前预加载:

TypeScript
// 用户悬停在按钮上时预加载
function preloadEditor() {
  const link = document.createElement('link');
  link.rel = 'modulepreload';
  link.href = '/editor.js';
  document.head.appendChild(link);
}

button.addEventListener('mouseenter', preloadEditor);

preload vs modulepreload vs prefetch#

特性 preload modulepreload prefetch
优先级 低(空闲时)
模块解析
CORS 处理 手动 自动 手动
适用场景 当前页面的关键资源 ES 模块 未来导航可能用到的资源
使用时机 首屏渲染关键路径 模块脚本入口和依赖 下一页面的资源

选择指南

  • 字体、关键 CSS、LCP 图片 → preload
  • ES 模块脚本 → modulepreload
  • 下一页面的资源、非关键路径资源 → prefetch

浏览器支持#

modulepreload 已被所有现代浏览器支持,详见 Can I Use。对于不支持的浏览器,标签会被安全忽略,不会造成错误。

注意事项#

不要过度使用:只预加载关键路径上的模块。预加载过多资源会与其他关键资源竞争带宽。

考虑缓存:对于已缓存的模块,modulepreload 的收益会降低。配合合理的缓存策略使用。

动态导入的权衡:对于按需加载的模块,使用 prefetch 可能比 modulepreload 更合适——前者优先级较低,不会影响首屏加载。


preloadmodulepreload 都是资源预加载工具,但解决的问题不同。preload 适用于字体、样式、图片等通用资源;modulepreload 则是 ES 模块的专属方案,解决模块依赖图带来的加载瀑布问题。理解两者的区别,才能在正确的场景使用正确的工具。

参考资料#

Footnotes#

  1. rel=preload - MDN Web Docs

  2. HTML Specification - Link types

  3. Preload critical assets - web.dev

  4. rel=modulepreload - MDN Web Docs

This browser prefers English.

You can switch the site to English and save that choice for future visits.