引言
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 考虑的元素类型包括:
<img>元素<image>元素内的<svg><video>元素的封面图- 通过
url()加载背景图的元素 - 包含文本节点的块级元素
<!-- 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 毫秒以内。
// 长任务阻塞主线程 → 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 以下。
/* ❌ 图片没有预留空间,加载后导致偏移 */
img {
/* 图片加载完成前高度为 0 */
}
/* ✅ 为媒体元素预留宽高比 */
img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}<!-- ✅ 使用明确的 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 |
响应式图片
<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 中的图片优化
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"
/>
);
}懒加载策略
<!-- 首屏图片: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。
字体加载策略
@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; /* 子集化 */
}预加载关键字体
<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。
子集化和可变字体
/* 可变字体:一个文件覆盖多字重,减少请求数 */
@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 与代码分割
动态导入
// ❌ 静态导入:所有代码打包在一起
import { heavyChart } from './heavy-chart';
// ✅ 动态导入:按需加载
button.addEventListener('click', async () => {
const { renderChart } = await import('./heavy-chart');
renderChart(data);
});React.lazy 与 Suspense
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 保障
// ✅ 具名导入,支持 tree shaking
import { debounce } from 'lodash-es';
// ❌ 默认导入,无法 tree shake
import _ from 'lodash';
// ✅ package.json 中声明副作用
// "sideEffects": ["*.css", "*.scss"]Bundle 分析
使用 @next/bundle-analyzer 或 webpack-bundle-analyzer 定期审计打包体积:
# Next.js 项目
ANALYZE=true pnpm build
# 通用方案
npx webpack-bundle-analyzer dist/stats.json五、CDN 与缓存策略
Cache-Control 策略
# 不可变资源(带 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 配置要点
- 启用 Brotli 压缩 — 比 Gzip 压缩率高 15%-20%
- 配置合理的 TTL — 静态资源长缓存,HTML 短缓存
- 启用 HTTP/2 或 HTTP/3 — 多路复用减少连接开销
- 回源优化 — 设置合理的回源超时和重试策略
Service Worker 缓存
// 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;
})
);
});六、关键渲染路径优化
消除渲染阻塞资源
<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 预取
<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 库
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
# 安装并运行 Lighthouse CI
npm install -g @lhci/cli
lhci autorun --collect.url=https://example.com --upload.target=temporary-public-storagePerformanceObserver
// 监控长任务
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 });八、优化清单
将上述策略浓缩为一份可执行的检查清单:
- 图片:使用 WebP/AVIF 格式,提供响应式
<picture>元素,显式设置width/height,非首屏图片使用loading="lazy" - 字体:使用
font-display: swap,预加载关键字体,采用可变字体减少文件数,子集化限制字符集 - JavaScript:路由级代码分割,第三方库按需导入,使用
async/defer延迟非关键脚本 - CSS:内联关键 CSS,异步加载完整样式表,移除未使用的样式
- CDN:启用 Brotli 压缩,配置合理的
Cache-Control头,使用 HTTP/2 或 HTTP/3 - HTML:使用
preconnect/dns-prefetch加速第三方连接,优先fetchpriority="high"标记关键资源 - 监控:接入 Web Vitals 库收集 RUM 数据,定期运行 Lighthouse 审计,设置性能预算并持续跟踪
结语
Web 性能优化是一个持续的过程,而非一次性的项目。Core Web Vitals 为我们提供了清晰的度量标准,但真正的优化需要深入到资源的加载、解析、渲染的每一个环节。
从图片格式选择到字体加载策略,从代码分割到 CDN 缓存配置——每一个细节的改进都会累积成可感知的用户体验提升。建议将性能优化纳入 CI/CD 流程,通过 Lighthouse CI 或自定义性能预算,确保每一次部署都在提升而非损害性能。
记住:毫秒级的优化,在百万级用户规模下就是小时级的价值。