资源预加载

preloadprefetchdns-prefetchpreconnect 有什么区别?应该如何使用它们优化资源加载?

核心结论

选择资源提示时,先判断两个问题:

  1. 资源是当前页面需要,还是后续页面可能需要?
  2. 已经知道具体资源 URL,还是只知道资源所在的域名
方式使用范围浏览器执行的工作优先级与特点典型场景
preload当前页面提前请求并缓存指定资源根据 as 对应的资源类型确定,可用 fetchpriority 调整关键字体、发现较晚的 LCP 图片、关键脚本
modulepreload当前页面提前获取并处理 ES Module,放入模块映射专门面向模块脚本入口模块、关键依赖模块
prefetch后续导航推测性地请求并缓存资源通常优先级很低,浏览器可以延迟或跳过下一路由的 chunk、下一页资源
preconnect当前页面或后续导航提前完成 DNS、TCP 和 TLS 连接准备不下载资源,成本高于 DNS 解析确定马上访问的第三方 CDN、字体域名
dns-prefetch当前页面或后续导航只提前解析 DNS成本最低,收益也最有限可能访问但不确定何时访问的第三方域名

可以简化记忆为:

当前页面 + 知道资源 URL  → preload / modulepreload
后续页面 + 知道资源 URL  → prefetch
确定即将访问某个域名     → preconnect
只是可能访问某个域名     → dns-prefetch

这些声明的本质是向浏览器提供额外信息。尤其是 prefetchpreconnectdns-prefetch,浏览器会根据网络状况、设备资源和自身策略决定是否完整执行。

1. Preload:提前发现当前页面的关键资源

preload 的价值不是让资源立即执行,而是解决资源发现得太晚的问题。

例如字体通常要等 CSS 下载、解析并匹配到文字后才能被发现;CSS 背景图也要等样式表解析后才能被发现。把这些当前页面确定会使用的关键资源写入 <head>,浏览器就能更早开始请求。

预加载字体

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

字体请求使用 CORS 模式,因此字体的 preload 通常需要添加 crossorigin,使预加载请求与之后真正使用字体时的请求模式一致。跨域字体的服务器还必须返回正确的 Access-Control-Allow-Origin 响应头;crossorigin 本身不能绕过 CORS。

预加载首屏关键图片

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

这更适合通过 CSS 背景、脚本等方式较晚发现的 LCP 图片。如果图片本来就是 HTML 中靠前的 <img>,浏览器的预加载扫描器已经能较早发现它,通常直接在图片上使用 fetchpriority="high" 更简单:

<img src="/images/hero.webp" fetchpriority="high" alt="活动主视觉">

fetchpriority="high" 只是优先级提示,应该只用于少量真正关键的资源,不能替代资源压缩、合理尺寸和 CDN 缓存。

Preload 只负责加载,不负责应用或执行

预加载的 CSS 仍然需要通过样式表链接应用,预加载的经典脚本也仍然需要通过 <script> 执行:

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

<link rel="preload" href="/scripts/app.js" as="script">
<script src="/scripts/app.js" defer></script>

后续请求配置一致时,浏览器会复用已经取得的资源,而不是再次下载。

as 为什么不能省略?

as 告诉浏览器资源的请求目标类型,它会影响:

  • 请求优先级;
  • Accept 请求头;
  • Content Security Policy 的匹配;
  • 预加载资源能否被后续请求正确复用。
资源配置
CSSas="style"
经典 JavaScriptas="script"
图片as="image"
字体as="font"
后续由 fetch() / XHR 请求的数据as="fetch",并保证 CORS 配置一致
ES Module使用 rel="modulepreload"

type 可以进一步声明 MIME 类型。如果浏览器不支持该类型,它可以直接跳过这次预加载;media 则可以避免浏览器在媒体条件不匹配时加载资源。

2. Modulepreload:为 ES Module 准备的预加载

ES Module 默认使用 CORS 模式,并且拥有模块映射、解析和编译等专门的处理流程。对于模块脚本,应优先使用 modulepreload,而不是普通的 preload as="script"

<link rel="modulepreload" href="/scripts/entry.js">
<script type="module" src="/scripts/entry.js"></script>

普通 preload 主要是提前获取并缓存响应;modulepreload 还会按照模块脚本的规则处理资源,并将其放入当前文档的模块映射中,供后续执行复用。

3. Prefetch:为后续导航做低成本猜测

prefetch 表示当前页面不需要该资源,但用户接下来很可能需要。浏览器可以降低它的优先级,优先保障当前页面请求:

<!-- 用户很可能进入个人中心,提前获取对应的路由 chunk -->
<link rel="prefetch" href="/scripts/profile-page.chunk.js">

<!-- 用户很可能访问的同站点页面 -->
<link rel="prefetch" href="/checkout">

需要注意:

  • prefetch 是推测性优化,不保证一定发起请求;
  • 即使已经请求,资源也不保证一直保留到下一次导航;
  • 请求仍然受到 HTTP 缓存头、CORS、CSP 和浏览器缓存分区策略影响;
  • 预测错误会浪费用户流量,因此只应该预取命中概率较高的资源;
  • 它不会自动执行脚本、应用样式,也不会解析被预取 HTML 中的全部子资源。

所以不要把当前页面马上要使用的资源交给 prefetch,否则它可能因为优先级过低而延迟关键内容。

4. DNS Prefetch 与 Preconnect

当具体资源 URL 还不能确定,但已知资源所在域名时,可以提前处理连接阶段。

<!-- 确定当前页面马上会从该域名获取资源 -->
<link rel="preconnect" href="https://fonts.example.com" crossorigin>

<!-- 只是可能向该域名发请求,先做成本较低的 DNS 解析 -->
<link rel="dns-prefetch" href="https://analytics.example.com">

一次新的 HTTPS 请求通常需要经历:

DNS 查询 → TCP 连接 → TLS 握手 → HTTP 请求与响应
  • dns-prefetch 只尝试提前完成 DNS 查询;
  • preconnect 尝试提前完成 DNS、TCP 和 TLS,但不下载具体资源;
  • 浏览器在资源受限时可能只完成部分连接准备,或者跳过提示;
  • preconnect 会消耗连接、CPU 和内存,不要为大量不确定域名建立空闲连接;
  • crossorigin 用于让连接的凭证模式与后续请求匹配,字体 CDN 是常见场景,但它并不代表服务器已经允许跨域。

5. 为什么 Preload 有时会重复下载?

浏览器复用 preload 时,不是只比较 URL。预加载请求与实际请求至少要在以下方面保持一致:

  • URL,包括查询参数和版本号;
  • 请求目标类型,即正确的 as
  • CORS 请求模式;
  • 凭证模式,即是否携带 Cookie 等信息;
  • 响应类型及最终选择的资源。

常见问题包括:

<!-- 错误:字体缺少 crossorigin,可能无法被后续字体请求复用 -->
<link rel="preload" href="/fonts/my-font.woff2" as="font">

<!-- 错误:预加载声明为 style,实际却作为 script 使用 -->
<link rel="preload" href="/scripts/app.js" as="style">

响应式图片还要确保 preload 最终选择的 URL 与 <img srcset> 实际选择结果一致,否则也可能下载两张图片。

6. 常见优化误区

误区一:Preload 就是最高优先级

preload 的核心是提前发现。浏览器会根据 as 对应的资源类型分配优先级,还可以参考 fetchpriority,并不是所有 preload 都一律最高优先级。

误区二:Preload 越多越好

preload 不会直接阻塞页面渲染,但它会占用网络带宽和连接资源。预加载过多非关键资源,可能反过来延迟 CSS、LCP 图片等真正关键的请求。

误区三:所有跨域资源都加crossorigin

是否添加 crossorigin,取决于资源最终的请求模式。盲目添加会导致 preload 与实际请求模式不一致,甚至让原本能展示的跨域图片加载失败。即使添加了它,跨域服务器仍然需要返回正确的 CORS 响应头。

误区四:看到 LCP 图片就一定 Preload

先在 Network 瀑布图中确认资源是否发现过晚。如果 <img> 已经位于 HTML 前部,重复声明 preload 的收益可能很小,优先检查 fetchpriority、图片尺寸、压缩格式和服务端响应时间。

误区五:同时 Preload 多个候选格式

不要同时预加载同一图片的 AVIF、WebP 和 JPEG 版本。浏览器可能支持不止一种格式,从而把多个候选资源都下载下来。应该只预加载页面最终会使用的资源。

7. Webpack 中的资源提示

Webpack 支持通过动态导入的魔法注释生成资源提示:

// 后续可能使用
import(/* webpackPrefetch: true */ "./ProfilePage");

// 当前导航确定需要
import(/* webpackPreload: true */ "./CriticalChart");

这是 Webpack 的构建能力,不是 JavaScript 语法本身。不同构建工具的生成策略可能不同,最终应检查构建产物和浏览器 Network 面板,确认提示生成时机、请求优先级以及是否发生重复下载。

实际优化流程

  1. 通过浏览器 Network 瀑布图确认关键资源是否发现过晚,而不是先批量添加资源提示。
  2. 对当前页面确定使用、但发现较晚的少量关键资源使用 preloadmodulepreload
  3. 对关键 LCP 图片谨慎使用 fetchpriority="high",并验证 LCP 是否真实改善。
  4. 对确定马上访问的第三方域名使用 preconnect,不太确定时使用成本更低的 dns-prefetch
  5. 只对命中概率高的后续导航资源使用 prefetch
  6. 检查控制台中的 unused preload 警告、Network 中的重复请求,并在真实网络环境下对比优化前后的指标。

面试回答模板

preload 用于当前导航确定需要的资源,作用是让浏览器更早发现并请求资源,但不会自动执行或应用资源;它的优先级由 as 对应的资源类型和 fetchpriority 共同影响。prefetch 用于后续导航可能需要的资源,优先级较低,浏览器可以根据运行环境延迟或跳过。preconnect 提前建立到目标域名的连接,dns-prefetch 则只提前进行 DNS 解析。实际使用时要保证 preload 的 URL、as、CORS 和凭证模式与最终请求一致,并通过 Network 瀑布图验证是否真的减少了资源发现和连接耗时。

参考资料