Link 预加载与性能优化

深入理解 link 标签的预加载机制:preload、prefetch、dns-prefetch、preconnect 等优化技术,提升页面加载性能和用户体验。

Link 预加载与性能优化

一、为什么需要预加载

1.1 延迟加载的局限性

现代 Web 应用普遍采用"必要才加载"的策略——JavaScript 代码分割、图片懒加载、路由懒加载等。这些技术有效减少了首屏加载资源量,但同时也带来了"需要时再加载"的延迟问题。当用户点击一个按钮触发懒加载模块时,往往需要等待数百毫秒才能看到响应。

预加载技术正是解决这一矛盾的方法:通过预测用户行为,在真正需要资源之前就开始加载。

1.2 预加载技术家族

技术 作用 实际效果
dns-prefetch DNS 预解析 提前完成域名解析
preconnect 预连接 DNS + TCP + TLS
preload 预加载 高优先级获取资源
prefetch 预取 低优先级获取资源

二、dns-prefetch

2.1 工作原理

dns-prefetch 指示浏览器提前对目标域名进行 DNS 解析。当后续需要从该域名请求资源时,DNS 解析已经完成,直接跳过 DNS 查询步骤。

<!-- 预解析第三方域名 -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
<link rel="dns-prefetch" href="https://static.example.com" />
<!-- DNS 预解析的典型应用场景 -->
<head>
  <!-- Google Fonts -->
  <link rel="dns-prefetch" href="https://fonts.gstatic.com" crossorigin />

  <!-- CDN 资源 -->
  <link rel="dns-prefetch" href="https://cdn.example.com" />

  <!-- 统计分析 -->
  <link rel="dns-prefetch" href="https://www.google-analytics.com" />
</head>

2.2 性能收益

DNS 解析通常需要 20-120ms。提前完成 DNS 解析意味着资源请求可以更快开始:

未优化流程:
  用户点击 → DNS 解析(50ms) → 建立连接 → 请求资源 → 等待响应

优化后流程:
  页面加载时 DNS 预解析 → 用户点击 → 建立连接 → 请求资源 → 等待响应

2.3 注意事项

  • dns-prefetch 是提示而非指令,浏览器可以选择忽略
  • 对于当前域名不需要预解析(浏览器已经解析过)
  • preconnect 相比,dns-prefetch 只做 DNS 解析,不建立连接

三、preconnect

3.1 比 dns-prefetch 更进一步

preconnect 不仅完成 DNS 解析,还建立 TCP 连接并进行 TLS 握手(如果是 HTTPS)。对于即将使用的跨域资源,这是最完整的预连接方式。

<!-- 预连接关键第三方资源 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

3.2 preconnect vs dns-prefetch

<!-- 只预解析 DNS -->
<link rel="dns-prefetch" href="https://api.example.com" />

<!-- 预解析 + 建立 TCP/TLS 连接 -->
<link rel="preconnect" href="https://api.example.com" />

对于高延迟的跨域请求,preconnect 可以节省整个连接的建立时间:

dns-prefetch 后首次请求:
  DNS(30ms) + TCP(50ms) + TLS(40ms) + 请求 = 延迟

preconnect 后首次请求:
  (已建立连接) + 请求 = 显著降低延迟

3.3 适用场景

<!-- WebSocket 连接 -->
<link rel="preconnect" href="wss://realtime.example.com" />

<!-- API 请求 -->
<link rel="preconnect" href="https://api.example.com" />

<!-- CDN 资源(首次加载)-->
<link rel="preconnect" href="https://cdn.example.com" />

3.4 谨慎使用

<!-- ❌ 过度使用 preconnect -->
<!-- 每个域名都 preconnect 会浪费资源 -->
<link rel="preconnect" href="https://domain1.com" />
<link rel="preconnect" href="https://domain2.com" />
<link rel="preconnect" href="https://domain3.com" />

<!-- ✅ 只预连接关键域名 -->
<!-- 明确知道会在短时间内使用的域名 -->
<link rel="preconnect" href="https://critical-api.com" />

浏览器对同时进行的 preconnect 数量有限制,过度使用可能导致关键连接被延迟。


四、preload

4.1 核心概念

preload 是最直接的预加载指令:告诉浏览器"这个资源在当前页面一定会用到,请现在就以高优先级获取"。

<!-- 预加载关键 CSS -->
<link rel="preload" href="/styles/main.css" as="style" />

<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" crossorigin />

<!-- 预加载关键 JavaScript -->
<link rel="preload" href="/js/app.js" as="script" />

4.2 as 属性的重要性

as 属性对于 preload 至关重要,它告诉浏览器资源的类型,使其能够:

  1. 正确设置请求头(如 Accept 用于内容协商)
  2. 正确设置请求优先级
  3. 正确应用 CSP(内容安全策略)
  4. 避免重复下载
<!-- ❌ 没有 as 属性 -->
<link rel="preload" href="/font.woff2" />
<!-- 浏览器无法确定资源类型,可能导致二次获取 -->

<!-- ✅ 有 as 属性 -->
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin />

4.3 有效的 as 值

as 值 资源类型
audio 音频文件
document HTML 文档(用于 iframe)
embed 嵌入资源
fetch fetch/XHR 请求
font 字体文件
image 图片
object 嵌入对象
script JavaScript 文件
style CSS 样式表
track WebVTT 字幕
video 视频文件
worker Web Worker

4.4 字体预加载的特殊考虑

<!-- 字体预加载必须使用 crossorigin -->
<!-- 否则会导致字体被获取两次 -->
<link rel="preload" href="/fonts/custom.woff2" as="font" crossorigin />

<!-- 带变体的字体 -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" crossorigin type="font/woff2" />

五、prefetch

5.1 与 preload 的区别

preload 用于当前导航必然需要的资源,prefetch 用于未来可能需要的资源。prefetch 以最低优先级获取,不会与当前资源竞争带宽。

<!-- preload:当前页面必需 -->
<link rel="preload" href="/js/app.js" as="script" />

<!-- prefetch:下个页面可能用到 -->
<link rel="prefetch" href="/js/editor.js" as="script" />

5.2 典型应用场景

<!-- 搜索结果页预取详情页资源 -->
<link rel="prefetch" href="/product/123" as="document" />

<!-- 首页预取文章页图片 -->
<link rel="prefetch" href="/images/hero-article.jpg" as="image" />

<!-- 列表页预取分页资源 -->
<link rel="prefetch" href="/api/items?page=2" as="fetch" />

5.3 使用注意

  • prefetch 的资源会被浏览器缓存(如果符合条件)
  • 浏览器可以根据空闲带宽智能决定是否真正执行 prefetch
  • 对于关键渲染路径资源,永远用 preload 而非 prefetch

六、实际应用案例

6.1 Google Fonts 优化

<head>
  <!-- 预连接fonts域名 -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

  <!-- 预加载特定字体文件(如果使用 woff2) -->
  <link rel="preload" href="/fonts/inter-var.woff2" as="font" crossorigin />
</head>

6.2 图片画廊预加载

<!-- 用户即将浏览的图片列表 -->
<img src="/gallery/1-thumb.jpg" alt="Image 1" />
<img src="/gallery/2-thumb.jpg" alt="Image 2" />

<!-- 预取高分辨率版本 -->
<link rel="prefetch" href="/gallery/1-full.jpg" as="image" />
<link rel="prefetch" href="/gallery/2-full.jpg" as="image" />

6.3 路由预加载

// React Router 风格的预加载实现
const prefetchRoute = (path) => {
  const link = document.createElement('link');
  link.rel = 'prefetch';
  link.href = `/routes${path}.js`;
  link.as = 'script';
  document.head.appendChild(link);
};

// 鼠标悬停时预加载
document.querySelectorAll('a[data-prefetch]').forEach(link => {
  link.addEventListener('mouseenter', () => {
    prefetchRoute(link.getAttribute('href'));
  }, { once: true });
});

七、面试高频问题

Q: preload 和 prefetch 的区别是什么?

回答要点:preload 用于当前页面必定需要的资源(关键 CSS、字体、核心 JS),以高优先级提前获取;prefetch 用于下个导航可能需要的资源(下一页 JS、图片),以低优先级在空闲时获取。preload 不应该滥用,prefetch 用于预测性加载。

Q: dns-prefetch 和 preconnect 的区别?

回答要点:dns-prefetch 只做 DNS 解析,preconnect 除了 DNS 还建立 TCP 连接并进行 TLS 握手。preconnect 更完整但也更消耗资源,应该只用于确定会使用的关键域名。

Q: link preload 会阻塞渲染吗?

回答要点:preload 本身不会阻塞渲染,它只是声明性地告诉浏览器提前获取资源。但被 preload 的资源(如 CSS)一旦获取完成,在应用时可能会阻塞渲染,这是由资源类型决定的,不是 preload 本身的问题。


参考资料

延展阅读