前端性能优化实战手册·第2篇:资源加载策略全解

《前端性能优化实战手册》系列 · 第 2 篇

第 1 篇我们把 Lighthouse 从 60 优化到了 95,靠的是减少体积。但体积再小,资源加载的方式不对,用户依然会觉得"慢"。

本篇我们进入传输层:HTTP 协议怎么选、浏览器预处理怎么用、CDN 和边缘计算怎么落地、Service Worker 怎么把二次访问变成"秒开"。


前言:为什么体积优化完,还是觉得慢?

答案很简单:体积优化解决的是"下载多少",资源加载策略解决的是"什么时候下载、以什么顺序下载、从哪下载"。

举个极端例子:你把所有 CSS 都内联进了 HTML,体积确实小了,但浏览器要等 HTML 解析完才能拿到样式------首屏直接 FOUC(无样式内容闪烁)。这就是加载策略没做对。

本篇的优化清单:

优化项 解决的问题 典型收益
HTTP/3 QUIC 高延迟网络下的握手开销 TTFB 降 30-50%
preconnect / dns-prefetch 第三方域名的连接建立延迟 首屏快 100-300ms
preload / prefetch 关键资源晚加载、非关键资源早占用 LCP 提升明显
fetchpriority 浏览器资源调度顺序错误 TBT 降低
CDN + 边缘计算 物理距离导致的延迟 全球访问统一快
Service Worker 二次访问重复下载 二次访问"秒开"

一、HTTP 协议层:HTTP/2 vs HTTP/3 QUIC

1.1 你还在用 HTTP/1.1 吗?

2026 年了,如果你的站点还在跑 HTTP/1.1,那资源加载这块你至少浪费了 30% 的性能。

HTTP/1.1 的核心问题是队头阻塞(Head-of-Line Blocking):同一个 TCP 连接上,前一个请求没完成,后面的请求就得排队。浏览器为了绕过这个限制,会开 6 个并发连接------但每个连接都有 TCP 握手 + TLS 握手的固定开销。

1.2 HTTP/2:多路复用

复制代码
HTTP/1.1:  6 个 TCP 连接,每个连接串行请求
[conn1] ╌─ req1 ── resp1 ── req2 ── resp2
[conn2] ╌─ req3 ── resp3
...

HTTP/2:   1 个 TCP 连接,所有请求并行(多路复用)
[conn1] ╌─ stream1 ─┐
        ╌─ stream2 ─┤ 帧交错传输,互不阻塞
        ╌─ stream3 ─┘

HTTP/2 用二进制分帧解决了应用层队头阻塞,一个连接上可以并行传输多个请求/响应。

但 HTTP/2 有个隐藏陷阱 :它只解决了应用层阻塞,TCP 层的队头阻塞还在。一旦某个 TCP 包丢失,整个连接上的所有流都要等这个包重传。在丢包率高的移动网络下,HTTP/2 甚至比 HTTP/1.1 还慢。

1.3 HTTP/3:QUIC 彻底解决队头阻塞

HTTP/3 把底层从 TCP 换成 UDP + QUIC

复制代码
HTTP/3 + QUIC:
┌─────────────────────────────────────┐
│  HTTP/3 (应用层,多路复用)            │
├─────────────────────────────────────┤
│  QUIC (运行在 UDP 上,每个流独立)     │
│   stream1 ╌─┐ 丢包只影响 stream1     │
│   stream2 ╌─┤ 其他流继续传输          │
│   stream3 ╌─┘                        │
├─────────────────────────────────────┤
│  UDP (无连接,无队头阻塞)             │
└─────────────────────────────────────┘

QUIC 的核心优势:

  1. 0-RTT / 1-RTT 连接建立 --- TCP + TLS 需要 3 次握手(至少 1.5 个 RTT),QUIC 在已有连接信息时可做到 0-RTT 恢复
  2. 流级独立 --- 一个流丢包不影响其他流
  3. 连接迁移 --- 手机从 WiFi 切到 4G,TCP 连接会断,QUIC 基于连接 ID 可以无缝迁移

2026 年的判断:主流 CDN(Cloudflare、AWS CloudFront、阿里云 CDN)都已支持 HTTP/3。如果你的站点是面向移动端用户的,上 HTTP/3 几乎是免费的 TTFB 提升。

nginx 复制代码
# Nginx 启用 HTTP/3(需 Nginx 1.25+)
server {
    listen 443 quic reuseport;
    listen 443 ssl;
    http2 on;

    ssl_protocols TLSv1.3;
    ssl_early_data on;  # 0-RTT 支持

    add_header Alt-Svc 'h3=":443"; ma=86400';  # 告诉浏览器支持 HTTP/3
}

二、浏览器预处理:别等浏览器"想起来"才加载

浏览器很聪明,但它不是你肚子里的蛔虫。资源放在哪、什么时候加载,你不告诉它,它就按默认策略来------通常不够快。

2.1 preconnect:提前建立连接

最典型的应用:你的静态资源在 CDN 域名 cdn.example.com,但 HTML 在 www.example.com

html 复制代码
<!-- 告诉浏览器:提前和 CDN 域名建立 TCP + TLS 连接 -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />

ROI:preconnect 一个跨域域名通常能省 100-300ms 的首屏时间。尤其是字体、第三方 SDK 这种"发现得晚但要得急"的资源。

⚠️ 坑点:preconnect 是有成本的(占用浏览器连接池),不要 preconnect 超过 4-6 个域名,更不要 preconnect 你其实不用的域名。

2.2 dns-prefetch:比 preconnect 更轻量

如果只需要省 DNS 解析时间(不想提前做 TLS 握手),用 dns-prefetch:

html 复制代码
<link rel="dns-prefetch" href="https://analytics.example.com" />

适用场景:你知道会用到、但不确定什么时候用的第三方域名(比如用户点击后才加载的聊天 SDK)。

2.3 preload:强制提前加载关键资源

preload 告诉浏览器:这个资源当前页面马上就要用,现在就加载,不要等

html 复制代码
<!-- 关键 CSS 提前加载 -->
<link rel="preload" href="/css/critical.css" as="style" />

<!-- 关键字体(配合 crossorigin,否则会重复下载) -->
<link
  rel="preload"
  href="/fonts/NotoSansSC-subset.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

<!-- 首屏首图 -->
<link rel="preload" as="image" href="/images/hero.avif" fetchpriority="high" />

关键:as 属性必须正确 ------as="style"as="script"as="font"as="image"。如果 as 写错,浏览器会以低优先级加载,等于白做。

⚠️ 最大坑:preload 滥用会适得其反。preload 一个非关键资源 = 抢了关键资源的带宽。只 preload LCP 相关的资源。

2.4 prefetch:为"下一个页面"做准备

prefetch 用于加载将来页面可能用到的资源(用户大概率会点进去的下一页):

html 复制代码
<!-- 用户大概率会进产品详情页,提前把它的 JS chunk 拉下来 -->
<link rel="prefetch" as="script" href="/assets/product-detail-chunk.js" />
js 复制代码
// Vue Router 的 prefetch(webpack magic comment)
const ProductDetail = () =>
  import(
    /* webpackChunkName: "product-detail" */
    /* webpackPrefetch: true */
    './views/ProductDetail.vue'
  );

适用场景:列表页 → 详情页。用户划列表时,浏览器空闲就把详情页的代码拉好了,点进去瞬间加载。

2.5 prerender:直接渲染下一个页面

比 prefetch 更激进------直接把下一个页面在后台渲染好:

html 复制代码
<link rel="prerender" href="/checkout" />

⚠️ 谨慎使用:prerender 消耗的资源(CPU、内存)远大于 prefetch。只用于转化率极高、几乎必点的下一步(如电商结算页)。


三、fetchpriority:让浏览器排对队

2023 年 Chrome 正式支持 fetchpriority 属性,这是直接告诉浏览器哪个资源更重要的杀手锏。

3.1 默认调度的问题

浏览器默认按资源类型给优先级:

  • CSS、字体:高
  • 同步 JS:高
  • 图片:低到中
  • 异步 JS:低

但现实是:你的 LCP 图片可能比某个非关键 CSS 更重要,浏览器却给它更低的优先级。

3.2 fetchpriority 实战

html 复制代码
<!-- LCP 图片:强制最高优先级 -->
<img src="/hero.avif" fetchpriority="high" alt="Hero" />

<!-- 首屏下方的图片:降低优先级,别抢带宽 -->
<img src="/below-fold.jpg" fetchpriority="low" loading="lazy" alt="" />

<!-- 非关键 JS:降低优先级 -->
<script src="/analytics.js" fetchpriority="low" defer></script>

<!-- 关键 JS:提升优先级 -->
<script src="/main-app.js" fetchpriority="high"></script>

配合 preload 的经典组合(解决 LCP 图片被低优先级拖累):

html 复制代码
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />

实测数据:给 LCP 图片加 fetchpriority="high",LCP 时间平均下降 5-15%


四、CDN 与边缘计算(Edge Functions)

4.1 CDN 不是"开了就完事"

很多团队 CDN 配置就是"把静态资源丢上去",但 CDN 的缓存策略直接决定你的 TTFB。

关键配置:合理的缓存 TTL

nginx 复制代码
# 静态资源(带 hash 命名):缓存 1 年,永不变
location /assets/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# HTML(每次都要最新的):不缓存或极短
location / {
    add_header Cache-Control "public, max-age=0, must-revalidate";
}

# 图片
location /images/ {
    add_header Cache-Control "public, max-age=86400";
}

immutable 关键字 很重要:告诉浏览器"这个文件永远不变,连 ETag 校验都省了"。带 hash 的构建产物(如 main.a1b2c3.js)天然适合。

4.2 边缘计算:把逻辑搬到离用户最近的地方

传统架构:

复制代码
用户(北京) → 源站(杭州) → 数据库(杭州)
         ≈ 30-50ms 网络延迟

边缘计算架构(Cloudflare Workers / Vercel Edge Functions):

复制代码
用户(北京) → 边缘节点(北京) → 直接响应
         ≈ 5ms 网络延迟

实战:用 Edge Function 做 A/B 测试分流(无需回源):

js 复制代码
// Vercel Edge Middleware
import { NextResponse } from 'next/server';

export function middleware(req) {
  const variant = req.cookies.get('ab-variant')?.value
    || (Math.random() < 0.5 ? 'a' : 'b');

  const res = NextResponse.next();
  res.cookies.set('ab-variant', variant, { maxAge: 86400 });

  // 直接改写响应头,边缘完成分流
  res.headers.set('x-ab-variant', variant);
  return res;
}

边缘计算能做的远不止 A/B 测试:

  • 边缘 SSR:在离用户最近的节点渲染 HTML
  • 边缘缓存:API 响应在边缘缓存,源站压力骤降
  • 边缘鉴权:JWT 校验在边缘完成,非法请求根本不进源站

五、Service Worker:让二次访问"秒开"

前面的所有优化,首次访问都能受益。但 Service Worker 是二次访问的王炸------它能让回访用户感觉页面是"本地应用"级的快。

5.1 四种核心缓存策略

策略 行为 适用场景
Cache First 先查缓存,没有再请求网络 静态资源(JS/CSS/图片)
Network First 先请求网络,失败降级缓存 实时数据(API 响应)
Stale-While-Revalidate 立即返回缓存,同时后台更新 产品列表、新闻 feed
Network Only 只走网络 支付、结账等强一致性场景

5.2 Stale-While-Revalidate 实战

这是最常用也最优雅的策略------用户永远先拿到旧数据(快),后台悄悄更新(新)

js 复制代码
// sw.js
const CACHE = 'v1';

self.addEventListener('fetch', (event) => {
  const { request } = event;

  // 只对 GET 请求做缓存
  if (request.method !== 'GET') return;

  event.respondWith(
    caches.match(request).then((cached) => {
      // 后台更新:无论缓存命中与否,都去网络拉最新版
      const fetchPromise = fetch(request)
        .then((response) => {
          // 只缓存有效响应
          if (response && response.status === 200) {
            const clone = response.clone();
            caches.open(CACHE).then((cache) => cache.put(request, clone));
          }
          return response;
        })
        .catch(() => cached); // 网络失败就返回缓存

      // 有缓存立即返回,同时后台更新
      return cached || fetchPromise;
    })
  );
});

5.3 用 Workbox 省掉手写样板

手写 Service Worker 容易出 bug。用 Google 的 Workbox:

js 复制代码
// sw.js --- Workbox
import { registerRoute } from 'workbox-routing';
import {
  StaleWhileRevalidate,
  CacheFirst,
  NetworkFirst,
} from 'workbox-strategies';

// 静态资源:缓存优先
registerRoute(
  ({ request }) =>
    ['style', 'script', 'worker'].includes(request.destination),
  new CacheFirst({ cacheName: 'static-assets' })
);

// API 数据:Stale-While-Revalidate
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new StaleWhileRevalidate({ cacheName: 'api-cache' })
);

// 关键页面:网络优先,失败降级
registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({ cacheName: 'pages' })
);

5.4 注册 Service Worker

js 复制代码
// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js').catch((err) => {
      console.error('SW registration failed:', err);
    });
  });
}

⚠️ 坑点 :Service Worker 只在 HTTPS(localhost 除外)下工作;更新 SW 后用户需要刷新才能加载新版本------用 skipWaiting() + clients.claim() 控制更新时机。


六、综合实战:一个真实的加载优化案例

把本篇 + 第 1 篇串起来,看一个真实项目的优化全流程:

复制代码
╔══════════════════════════════════════════════╗
║  优化前                                        ║
╠══════════════════════════════════════════════╣
║  TTFB:        800ms  (HTTP/1.1, 源站在杭州)    ║
║  FCP:          3.2s                           ║
║  LCP:          5.8s                           ║
║  二次访问 FCP: 2.9s   (每次都重新下载)          ║
╚══════════════════════════════════════════════╝

    ↓ 第1篇:体积优化(图片/JS/CSS/字体/CLS)
    ↓ 本篇:资源加载策略

╔══════════════════════════════════════════════╗
║  优化后                                        ║
╠══════════════════════════════════════════════╣
║  TTFB:        120ms  (HTTP/3 + 边缘节点)       ║
║  FCP:          1.1s   (preload + preconnect)   ║
║  LCP:          2.1s   (fetchpriority + 图片优化)║
║  二次访问 FCP: 0.3s   (Service Worker 缓存)    ║
╚══════════════════════════════════════════════╝

  关键指标提升:
  TTFB:  800ms → 120ms   ↓ 85%
  FCP:   3.2s  → 1.1s    ↓ 66%
  LCP:   5.8s  → 2.1s    ↓ 64%
  二次访问: 2.9s → 0.3s   ↓ 90%

这个项目的完整优化清单:

html 复制代码
<!-- index.html 头部 -->
<head>
  <!-- 1. 提前连接 CDN 和字体域名 -->
  <link rel="preconnect" href="https://cdn.example.com" crossorigin />
  <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />

  <!-- 2. 预加载 LCP 图片和关键字体 -->
  <link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />
  <link
    rel="preload"
    as="font"
    href="/fonts/NotoSansSC-subset.woff2"
    type="font/woff2"
    crossorigin
  />

  <!-- 3. 关键 CSS 内联(见第1篇) -->

  <!-- 4. 预取下一页 chunk -->
  <link rel="prefetch" as="script" href="/product-detail-chunk.js" />
</head>
js 复制代码
// 服务端:HTTP/3 + 边缘 + 缓存策略
// (见上文 Nginx / Edge Middleware / CDN 配置)

// 客户端:注册 Service Worker
// (见上文 Workbox 配置)

本篇总结 & 下篇预告

本篇核心要点

  1. 协议层优先 --- HTTP/3 QUIC 免费省 TTFB,移动端尤其明显
  2. preconnect 是关键 --- 跨域资源提前建连,省 100-300ms
  3. preload 只给 LCP --- 滥用会抢带宽,得不偿失
  4. fetchpriority 兜底 --- 直接告诉浏览器哪个资源更重要
  5. CDN 缓存策略要细 --- 带 hash 的资源缓存 1 年 + immutable
  6. 边缘计算把逻辑推到用户门口 --- A/B 测试、SSR、鉴权都能在边缘做
  7. Service Worker 是二次访问的必杀 --- Stale-While-Revalidate 最优雅

🔜 下篇预告

第 3 篇:《渲染性能深度优化》

  • 长列表虚拟化(Virtual List)实战
  • 防抖/节流的正确姿势与 requestIdleCallback
  • Web Worker 把重计算搬出主线程
  • React 并发渲染(Concurrent Mode)与 useTransition
  • INP 指标优化全攻略

📌 《前端性能优化实战手册》系列

  • ✅ 第 1 篇:从 Lighthouse 60 到 95 的完整路径
  • ✅ 第 2 篇:资源加载策略全解(本文)
  • 🔜 第 3 篇:渲染性能深度优化
  • 🔜 第 4 篇:构建产物体积治理
  • 🔜 第 5 篇:性能监控与 CI/CD 集成
相关推荐
浅水壁虎1 小时前
vue基础(第二章 )
前端·javascript·vue.js
界面开发小八哥1 小时前
界面控件DevExtreme v26.1新版亮点——支持Angular 22
前端·javascript·angular.js·devexpress·ui开发·devextreme
Getflare1 小时前
前端 + UI 设计 + AI:这不是三个工种,是一个新三角能力模型(附自检清单)
前端·人工智能·ui
oil欧哟1 小时前
我做了一个 Vibe Coding 术语学习站:VibeHub
前端·ai·agent·独立开发·vibe coding
审小匠OpenCPAi2 小时前
银行流水核查怎么自动化?单边匹配、双向勾稽与图聚类异常检测的工程对比
java·前端·人工智能·python·审计
Revolution612 小时前
一个公共表格组件,是怎么一步步失控的
前端·前端工程化
腻害兔2 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:CRM 客户关系模块深度解析——从线索到回款,一套完整的 B2B 销售闭环是怎么搭的?
java·前端·javascript·vue.js·产品经理·ai编程
whyfail2 小时前
前端学 Spring Boot(2):一次点击,如何穿过整个后端?
前端·spring boot·后端
qq_297574673 小时前
RuoYi框架二次开发系列(五)性能优化、Redis缓存实战、分布式部署、接口限流与安全加固生产方案
redis·缓存·性能优化