背景:为什么 LCP 成了瓶颈
去年底,我们接手了一个内容型电商项目。上线后 Google Search Console 的 Core Web Vitals 报告显示:移动端 LCP 中位数为 4.2 秒,CLS 0.21,INP 280ms。三项指标全部标红,其中 LCP 是最大短板。
Core Web Vitals 包含 LCP(最大内容绘制)、CLS(累积布局偏移)和 INP(交互到下一次绘制)。LCP 衡量加载性能,CLS 衡量视觉稳定性,INP 衡量交互响应能力。
我们的目标很明确:把 LCP 压到 2.5 秒以内,同时把 CLS 和 INP 拉回绿色区间。
诊断:用数据锁定关键渲染路径
1. 实验室与现场数据结合
先用 Lighthouse 和 WebPageTest 跑基线,同时接入 web-vitals 库收集真实用户监控(RUM)数据。
import { onLCP, onCLS, onINP } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
delta: metric.delta,
});
// 使用 sendBeacon 避免阻塞卸载
navigator.sendBeacon('/analytics', body);
}
onLCP(sendToAnalytics);
onCLS(sendToAnalytics);
onINP(sendToAnalytics);2. 定位 LCP 元素
通过 PerformanceObserver 捕获 LCP 候选元素,发现移动端 LCP 是首屏轮播图的第一张图片,桌面端则是 H1 标题。
进一步分析瀑布图,发现三个核心问题:
- 关键渲染路径过长:CSS 阻塞渲染,首屏图片请求发起太晚(被 JS 和字体请求挤占)。
- 图片未优化:首屏图片是 1.8MB 的 PNG,且没有设置
fetchpriority。 - 字体加载导致 FOIT:自定义字体在 3.2 秒才可用,期间文字不可见。
优化实施:六个关键动作
1. 关键渲染路径优化
- 内联关键 CSS:将首屏渲染所需的 CSS 提取出来,直接内联到
<head>,其余 CSS 异步加载。 - 延迟非关键 JS:把非首屏交互的 JS 标记为
defer或动态import()。 - 预连接关键域名:对图片 CDN、字体 CDN 添加
preconnect和dns-prefetch。
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="dns-prefetch" href="https://fonts.example.com">
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.avif" as="image" fetchpriority="high">2. 图片优化
- 首屏图片改用 AVIF/WebP,配合
<picture>回退。 - 使用
srcset和sizes提供响应式图片,移动端只加载 800px 宽版本。 - 显式设置
width和height避免 CLS。 - 对 LCP 图片设置
fetchpriority="high",并移除loading="lazy"。
3. 字体优化
- 使用
font-display: swap避免 FOIT。 - 子集化字体,只保留拉丁字符和常用符号,体积从 180KB 降到 28KB。
- 预加载关键字体文件。
4. JS 拆包与按需加载
- 用 Webpack 的
splitChunks将 vendor 和业务代码分离。 - 路由级懒加载,首屏只加载必要模块。
- 移除未使用的 polyfill,使用
browserslist精准控制目标浏览器。
5. CDN 与缓存策略
- 静态资源全部上 CDN,开启 Brotli 压缩。
- 对带 hash 的文件设置
Cache-Control: max-age=31536000, immutable。 - HTML 使用
Cache-Control: no-cache配合 ETag。
6. 预连接与预加载
- 对第三方域名(支付、统计)使用
preconnect。 - 对首屏关键资源使用
preload,但避免过度预加载导致带宽竞争。
优化前后数据对比
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| LCP(移动端) | 4.2s | 1.3s | -69% |
| CLS | 0.21 | 0.02 | -90% |
| INP | 280ms | 90ms | -68% |
| 首屏图片体积 | 1.8MB | 120KB | -93% |
| 字体加载时间 | 3.2s | 0.4s | -87% |
| JS 首包体积 | 450KB | 180KB | -60% |
数据来自 30 天 RUM 采样,移动端 4G 网络,样本量约 12 万次页面浏览。
常见问题
LCP 优化后,为什么 INP 也提升了?
因为 JS 拆包减少了主线程阻塞,交互事件能更快被处理。INP 对长任务非常敏感,首包体积下降直接降低了长任务出现的概率。
预加载所有关键资源会不会适得其反?
会。preload 会占用带宽和优先级,如果预加载了非首屏资源,反而会推迟 LCP 图片的加载。建议只对 LCP 图片、关键字体和首屏 CSS 使用。
图片已经用了 WebP,为什么 LCP 还是慢?
检查是否设置了 fetchpriority="high",以及是否被 loading="lazy" 延迟。另外,图片 CDN 的响应时间、是否命中缓存也会影响 LCP。
CLS 优化后还会反弹吗?
会。如果后续新增了动态插入的内容(如广告、推荐位),没有预留空间,CLS 会再次升高。建议在 CI 中加入 Lighthouse 断言,防止回归。
评论 (0)