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 至关重要,它告诉浏览器资源的类型,使其能够:
- 正确设置请求头(如
Accept用于内容协商) - 正确设置请求优先级
- 正确应用 CSP(内容安全策略)
- 避免重复下载
<!-- ❌ 没有 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 本身的问题。