引言

Web 性能直接影响用户体验、搜索引擎排名和业务转化率。Google 的研究表明,页面加载时间从 1 秒增加到 3 秒,跳出率会增加 32%;而从 1 秒增加到 6 秒,跳出率会飙升 106%。

2020 年,Google 推出了 Core Web Vitals 指标集,为 Web 性能评估提供了统一标准。本文将深入探讨这些核心指标,并结合实战场景,给出系统性的优化策略。


一、理解 Core Web Vitals

Core Web Vitals 是 Google 定义的一组关键性能指标,由三个维度组成:

Largest Contentful Paint (LCP)

LCP 衡量页面主要内容加载速度,应该在页面开始加载后的 2.5 秒内完成。

LCP 考虑的元素类型包括:

html
<!-- LCP 通常会是首屏中的大图或标题文字 -->
<main>
  <h1>欢迎来到我的博客</h1>
  <img src="/hero-banner.webp" alt="Hero Banner" />
</main>

Interaction to Next Paint (INP)

INP 取代了 FID,于 2024 年 3 月成为 Core Web Vitals 正式指标。它衡量页面在整个生命周期内对所有用户交互的响应延迟,目标是 200 毫秒以内。

javascript
// 长任务阻塞主线程 → INP 恶化
function handleHeavyCalculation() {
  // ❌ 同步阻塞主线程
  for (let i = 0; i < 10000000; i++) {
    // 密集计算...
  }
}

// ✅ 使用 requestAnimationFrame 拆分任务
function handleOptimized() {
  requestAnimationFrame(() => {
    processChunk(data, 0);
  });
}

Cumulative Layout Shift (CLS)

CLS 衡量页面的视觉稳定性,计分从 0(无偏移)到正无穷,目标是 0.1 以下。

css
/* ❌ 图片没有预留空间,加载后导致偏移 */
img {
  /* 图片加载完成前高度为 0 */
}

/* ✅ 为媒体元素预留宽高比 */
img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}
html
<!-- ✅ 使用明确的 width 和 height 属性 -->
<img src="photo.webp" width="1200" height="675" alt="风景照片" loading="lazy" />

二、图片优化

图片通常占据页面总字节数的 50%-70%,是最值得优化的资源类型。

选择正确的格式

格式 压缩率 浏览器支持 适用场景
JPEG 中等 全部 照片、复杂图像
WebP 高(比 JPEG 小 25%-35%) 97%+ 照片替代 JPEG/PNG
AVIF 极高(比 WebP 小 20%-30%) 93%+ 首选现代格式
PNG 无损 全部 Logo、图标、透明图
SVG 矢量 全部 图标、插图、Logo

响应式图片

html
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img
    src="/hero.jpg"
    alt="首屏横幅"
    width="1200"
    height="630"
    loading="eager"
    fetchpriority="high"
    decoding="async"
  />
</picture>

Next.js 中的图片优化

tsx
import Image from 'next/image';

function HeroBanner() {
  return (
    <Image
      src="/hero.jpg"
      alt="首屏横幅"
      width={1200}
      height={630}
      priority
      placeholder="blur"
      blurDataURL="data:image/webp;base64,UklGRiQAAABXRUJQ..."
      sizes="(max-width: 768px) 100vw, 1200px"
    />
  );
}

懒加载策略

html
<!-- 首屏图片:eager 加载 -->
<img src="hero.webp" loading="eager" fetchpriority="high" alt="首屏图片" />

<!-- 折叠线以下:lazy 加载 -->
<img src="article-photo.webp" loading="lazy" alt="文章配图" />

<!-- iframe 懒加载 -->
<iframe src="https://www.youtube.com/embed/xxx" loading="lazy" title="视频教程"></iframe>

三、字体加载优化

字体加载不当会阻塞渲染、造成 FOIT/FOUT 问题,甚至影响 CLS。

字体加载策略

css
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Variable.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap; /* 关键:立即使用回退字体,字体加载后切换 */
  unicode-range: U+0000-00FF, U+0131, U+0152-0153; /* 子集化 */
}

预加载关键字体

html
<head>
  <!-- 预加载关键字体文件 -->
  <link
    rel="preload"
    href="/fonts/Inter-Variable.woff2"
    as="font"
    type="font/woff2"
    crossorigin="anonymous"
  />
</head>

font-display 取值对比

值 阻塞期 交换期 行为
auto 由浏览器决定 由浏览器决定 默认行为
block ~3s 无限 不可见文字 → 字体
swap ~100ms 无限 立即显示回退 → 交换
fallback ~100ms ~3s 短阻塞,不交换
optional ~100ms 无 字体可用则用

推荐:正文用 swap,图标/品牌文字用 optional。

子集化和可变字体

css
/* 可变字体:一个文件覆盖多字重,减少请求数 */
@font-face {
  font-family: 'Noto Sans SC';
  src: url('/fonts/NotoSansSC-VF.woff2') format('woff2-variations');
  font-weight: 400 700;
  font-display: swap;
}

/* 子集化:仅包含页面所需字符 */
@font-face {
  font-family: 'Custom Serif';
  src: url('/fonts/custom-serif-latin.woff2') format('woff2');
  unicode-range: U+0020-007F; /* 仅拉丁基本字符 */
  font-display: swap;
}

四、JavaScript 与代码分割

动态导入

javascript
// ❌ 静态导入:所有代码打包在一起
import { heavyChart } from './heavy-chart';

// ✅ 动态导入:按需加载
button.addEventListener('click', async () => {
  const { renderChart } = await import('./heavy-chart');
  renderChart(data);
});

React.lazy 与 Suspense

tsx
import { lazy, Suspense } from 'react';

const DashboardChart = lazy(() => import('./DashboardChart'));
const SettingsPanel = lazy(() => import('./SettingsPanel'));

function App() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <DashboardChart />
    </Suspense>
  );
}

Tree Shaking 保障

javascript
// ✅ 具名导入,支持 tree shaking
import { debounce } from 'lodash-es';

// ❌ 默认导入,无法 tree shake
import _ from 'lodash';

// ✅ package.json 中声明副作用
// "sideEffects": ["*.css", "*.scss"]

Bundle 分析

使用 @next/bundle-analyzer 或 webpack-bundle-analyzer 定期审计打包体积:

bash
# Next.js 项目
ANALYZE=true pnpm build

# 通用方案
npx webpack-bundle-analyzer dist/stats.json

五、CDN 与缓存策略

Cache-Control 策略

code
# 不可变资源(带 hash 的 JS/CSS/字体)
Cache-Control: public, max-age=31536000, immutable

# HTML 文档
Cache-Control: public, max-age=0, must-revalidate

# API 响应
Cache-Control: private, max-age=60, stale-while-revalidate=300

CDN 配置要点

  1. 启用 Brotli 压缩 — 比 Gzip 压缩率高 15%-20%
  2. 配置合理的 TTL — 静态资源长缓存,HTML 短缓存
  3. 启用 HTTP/2 或 HTTP/3 — 多路复用减少连接开销
  4. 回源优化 — 设置合理的回源超时和重试策略

Service Worker 缓存

javascript
// sw.js — 缓存优先策略
const CACHE_NAME = 'v2-static-assets';

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      // 缓存命中:立即返回,同时后台更新
      const fetchPromise = fetch(event.request).then((response) => {
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, response.clone());
        });
        return response;
      });
      return cached || fetchPromise;
    })
  );
});

六、关键渲染路径优化

消除渲染阻塞资源

html
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <!-- 关键 CSS 内联 -->
  <style>
    /* 首屏必须的样式(< 14KB) */
    body { margin: 0; font-family: system-ui; }
    .hero { min-height: 100vh; display: grid; place-items: center; }
  </style>

  <!-- 非关键 CSS 异步加载 -->
  <link
    rel="preload"
    href="/styles/full.css"
    as="style"
    onload="this.onload=null;this.rel='stylesheet'"
  />
  <noscript><link rel="stylesheet" href="/styles/full.css" /></noscript>

  <!-- JavaScript 延迟执行 -->
  <script src="/analytics.js" defer></script>
  <script src="/app.js" type="module"></script>
</head>

预连接与 DNS 预取

html
<head>
  <!-- 提前建立连接 -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

  <!-- DNS 预解析(成本比 preconnect 低) -->
  <link rel="dns-prefetch" href="https://api.example.com" />

  <!-- 预取下一页资源 -->
  <link rel="prefetch" href="/page-2.html" as="document" />
</head>

七、监控与度量

Web Vitals 库

javascript
import { onLCP, onINP, onCLS } from 'web-vitals';

function sendToAnalytics({ name, value, id, delta }) {
  // 发送到 Google Analytics、自定义后端等
  const body = JSON.stringify({
    name,
    value: Math.round(name === 'CLS' ? value * 1000 : value),
    id,
    delta: Math.round(delta),
  });

  navigator.sendBeacon('/api/vitals', body);
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

Lighthouse CI

bash
# 安装并运行 Lighthouse CI
npm install -g @lhci/cli
lhci autorun --collect.url=https://example.com --upload.target=temporary-public-storage

PerformanceObserver

javascript
// 监控长任务
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.warn(`长任务:${entry.duration}ms`, entry);
  }
});
observer.observe({ type: 'longtask', buffered: true });

// 监控资源加载
const resourceObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    if (entry.duration > 2000) {
      console.warn(`慢资源:${entry.name} — ${entry.duration}ms`);
    }
  });
});
resourceObserver.observe({ type: 'resource', buffered: true });

八、优化清单

将上述策略浓缩为一份可执行的检查清单:


结语

Web 性能优化是一个持续的过程,而非一次性的项目。Core Web Vitals 为我们提供了清晰的度量标准,但真正的优化需要深入到资源的加载、解析、渲染的每一个环节。

从图片格式选择到字体加载策略,从代码分割到 CDN 缓存配置——每一个细节的改进都会累积成可感知的用户体验提升。建议将性能优化纳入 CI/CD 流程,通过 Lighthouse CI 或自定义性能预算,确保每一次部署都在提升而非损害性能。

记住:毫秒级的优化,在百万级用户规模下就是小时级的价值。