Web Vitals(LCP、INP、CLS)详解
在性能优化里,我们经常会听到一句话:“不要只优化你以为的性能,要优化用户真实感受到的体验。”
Web Vitals 就是一套用来衡量“用户体验是否好”的指标体系,其中最核心的三个指标(也叫 Core Web Vitals)是:
- LCP(Largest Contentful Paint):页面“看起来加载好了没?”
- INP(Interaction to Next Paint):页面“点了之后响应快不快?”
- CLS(Cumulative Layout Shift):页面“会不会乱跳、误触?”
这篇文档会把三者的定义、判定阈值、如何测量、如何定位、如何优化、如何落地监控讲清楚,并在文末给出面试高频问答。
1. 三个指标先用一张表记住
| 指标 | 关注点 | 用户直观感受 | 好(Good) | 需改进(NI) | 差(Poor) |
|---|---|---|---|---|---|
| LCP | 加载体验 | 首屏“最大内容”出现得快不快 | ≤ 2.5s | 2.5s ~ 4.0s | > 4.0s |
| INP | 交互体验 | 点击/输入后多久“有反应” | ≤ 200ms | 200ms ~ 500ms | > 500ms |
| CLS | 视觉稳定性 | 页面会不会“抖动/位移” | ≤ 0.1 | 0.1 ~ 0.25 | > 0.25 |
- LCP:Largest → “最大的那块内容”出来的时间
- INP:Interaction → “交互后”到“下一次绘制”
- CLS:Layout Shift → 页面“布局偏移”的累计程度
2. 先搞清楚:实验室数据 vs 真实用户数据
很多同学会遇到这种情况:Lighthouse 跑分很好,但线上用户仍然觉得卡;或者本地复现不了线上问题。原因通常是数据来源不同:
- 实验室数据(Lab):在可控环境里跑出来的指标(如 Lighthouse),优点是可复现、方便定位;缺点是不等于真实用户。
- 真实用户数据(Field / RUM):来自真实用户设备与网络的指标(如你自己做的 RUM 埋点、或者使用 CrUX 数据),优点是最贴近真实体验;缺点是噪音多、定位要配合更多上下文。
Core Web Vitals 通常关注“大多数用户的体验”,不是平均值。实践中经常用 P75(75th percentile) 来判断页面是否达标:
如果 P75 仍然不好,说明至少 25% 的用户体验很糟糕,需要优先处理。
3. LCP:最大内容绘制(Largest Contentful Paint)
3.1 LCP 在衡量什么?
LCP 记录的是:从用户开始打开页面,到视口内最大的内容元素完成渲染的时间。
这个“最大内容”通常是:
- 首屏大图(Hero Image)
- 首屏最大的文本块(大标题/大段文字)
它回答的问题是:“页面什么时候看起来‘主要内容已经出来了’?”
LCP 只关注“首屏最大内容绘制完成”,并不要求所有资源加载完(比如评论区、推荐列表、埋点脚本都可能还在加载)。
3.2 LCP 由哪些部分组成?
把 LCP 理解成一条链路会更好排查(常见情况是某一段拖后腿):
实践里,你可以把 LCP 优化拆成三类:
- 后端/网络慢(TTFB 高)
- 资源下载慢(关键 CSS/JS/图片慢)
- 渲染慢(主线程忙、渲染阻塞)
3.3 常见导致 LCP 变差的原因
- 首屏大图太大:体积大、格式不合适、未压缩、未走 CDN
- 首屏图/关键 CSS 没有高优先级:被其它资源抢占带宽
- 渲染阻塞资源太多:同步 JS、阻塞 CSS、第三方脚本
- TTFB 高:服务器慢、无缓存、冷启动、跨地域
- 客户端渲染成本高:首屏需要跑很多 JS 才能看到内容(CSR + 大 bundle)
3.4 LCP 优化清单(从最常见到更深入)
1)把“LCP 元素”找出来
先确认你的 LCP 元素是谁(通常是首屏大图或大标题)。如果找错对象,优化很容易“用力用偏”。
常用工具:
- Chrome DevTools → Performance 录制 → 查看 LCP 标记
- Lighthouse 报告 → 看 “Largest Contentful Paint element”
- 线上:用
web-vitals埋点把element/url(如果可用)等信息带上报
2)如果 LCP 是图片:优先做图片链路优化
最有效、最常见的一组组合拳:
- 使用现代格式(如 WebP/AVIF)
- 合理尺寸(不要用 4000px 的图显示成 400px)
- 开启 CDN 与缓存(
Cache-Control/ETag) - 首屏 LCP 图不要懒加载(不要
loading="lazy") - 需要时对 LCP 图做“提权”:
fetchpriority="high"或<link rel="preload" as="image">
<!-- 示例:为首屏大图预留尺寸 + 提升优先级 -->
<img
src="/img/hero.webp"
width="1200"
height="600"
fetchpriority="high"
alt="首屏主视觉"
/>
“全站图片都上 loading=lazy”会让首屏大图也延后下载,直接拖慢 LCP。
经验:首屏关键图 eager,其它图 lazy。
3)如果 TTFB 高:先从服务端/缓存下手
- 开启页面/接口缓存(CDN 缓存、反向代理缓存、应用层缓存)
- 压缩传输(Brotli/Gzip)
- 减少重定向
- 让静态资源就近(CDN,正确的缓存策略)
4)减少渲染阻塞:把关键路径变短
- 减少首屏必须执行的 JS(拆包、按需加载、延后第三方)
- 抽取关键 CSS(Critical CSS),非关键 CSS 延后
- 避免在首屏执行大量同步计算(如大 JSON 解析、复杂 diff)
LCP 优化的核心目标是:让“首屏最大内容”更早下载、更早渲染。
所以你应该优先优化“关键路径”,而不是把时间花在首屏看不到的资源上。
4. INP:交互到下一次绘制(Interaction to Next Paint)
4.1 INP 在衡量什么?
当用户点击、触摸、键盘输入时,页面需要执行 JS、更新 DOM、完成渲染。INP 记录的是:
一次交互从发生开始,到页面完成“下一次绘制”(用户能看到反馈)所花的时间。
它回答的问题是:“用户操作以后,页面多久才真正‘有反应’?”
FID(First Input Delay)只关注“第一次交互”的等待时间,不包含事件回调执行与渲染时间。
INP 会覆盖更完整的链路,更符合用户体感,也更能反映长期交互性能(尤其是 SPA/重交互页面)。
4.2 INP 可以拆成三段来看
因此 INP 变差,通常意味着:主线程太忙(长任务多、JS 过重、渲染成本高)。
4.3 常见导致 INP 变差的原因
- 长任务(Long Task)多:一次 JS 执行占用主线程很久(例如 > 50ms)
- 事件回调里做了大量工作:复杂计算、同步请求、频繁 DOM 操作
- 大量第三方脚本抢主线程
- React/Vue 组件更新范围过大(一次交互触发全局重渲染)
- 动画/滚动监听写法不当,引发频繁同步布局
4.4 INP 优化清单(“让主线程空出来”)
1)先用工具定位“哪一次交互最慢”
- Chrome DevTools → Performance → 关注 Interactions(交互事件)与 Main(主线程任务)
- 线上:用
web-vitals上报 INP,同时带上路由/页面/设备信息,找出最差场景
2)把长任务切碎:避免一次占用主线程太久
思路:把大任务拆成多个小任务,让浏览器有机会渲染下一帧。
// 示例:把一次很重的工作拆分,避免阻塞交互与渲染
function processInChunks(items, handler) {
let index = 0;
function run() {
const start = performance.now();
while (index < items.length && performance.now() - start < 8) {
handler(items[index++]);
}
if (index < items.length) setTimeout(run, 0);
}
run();
}
拆分任务不是“更快完成”,而是“更快给用户反馈”。对交互体验来说,这往往更重要。
3)减少不必要的渲染与 DOM 操作
- 减少一次交互引发的更新范围(组件拆分、避免全局状态滥用)
- 避免在事件回调里频繁读写布局属性(
offsetWidth/getBoundingClientRect等) - 列表很长时做虚拟列表(见《虚拟列表》相关文档)
4)把非关键工作移出主线程
- Web Worker:把大计算放到 worker
- 非关键埋点/日志:用
navigator.sendBeacon或延后执行 - 第三方脚本:延后加载、按需加载、评估是否必须
5. CLS:累计布局偏移(Cumulative Layout Shift)
5.1 CLS 在衡量什么?
CLS 衡量的是:页面在加载与运行过程中,**可见元素发生“意外位移”**的程度。
用户的直观感受通常是:
- 页面内容突然下移,导致“点错”
- 文本突然抖动,阅读被打断
- 列表突然插入一块内容,视线被拉走
5.2 CLS 是怎么计算的?
单次布局偏移分数(layout shift score)大致可以理解为:
布局偏移分数 = 影响面积比例(impact fraction) × 位移距离比例(distance fraction)
页面的 CLS 则会把一段时间内的多次偏移归为“会话窗口(session window)”,取其中最大的一段作为最终值(避免把整页生命周期所有偏移无限累加)。
如果某次位移发生前 500ms 内有用户输入(recent input),通常不计入 CLS。
也就是说:用户主动触发的布局变化(比如展开手风琴)不应算“意外位移”。
5.3 常见导致 CLS 变差的原因
- 图片/视频没有尺寸,加载后把内容“顶开”
- 广告/卡片异步插入,没有提前占位
- Web 字体加载后替换字体,导致文字尺寸变化
- 首屏上方插入 banner、提示条(尤其是接口返回后才决定展示)
- 动画使用
top/left/height等会影响布局的属性
5.4 CLS 优化清单(“提前占位 + 避免顶格插入”)
1)所有媒体元素都要“可预测尺寸”
<img>写width/height(或使用 CSSaspect-ratio)- 视频、iframe、广告位都要预留容器高度
<!-- 示例:写死宽高,浏览器可在图片加载前就占位 -->
<img src="/img/card.webp" width="400" height="240" alt="卡片图" />
2)避免在视口上方“插入”内容
如果必须展示(如顶部通知条),更推荐:
- 使用覆盖层(overlay),不挤压布局
- 或在页面初始就预留占位,后续只是“显示/隐藏”
3)字体加载策略要合理
- 使用
font-display: swap减少不可见时间 - 选择指标更稳定的字体,或使用字体预加载(在确有必要时)
4)动画尽量用 transform/opacity
transform/opacity通常不会触发布局变化(更平滑)- 避免用
top/left/width/height做动画导致内容被推挤
6. 实战:用 web-vitals 采集并上报 LCP/INP/CLS
线上性能优化离不开“可观测性”:你需要知道用户真实遇到的 LCP/INP/CLS 是多少、集中在哪些页面、哪些设备、哪些网络。
一个推荐的落地方式是使用 web-vitals 库做 RUM 采集,然后上报到你的监控平台(自建或第三方)。
// 示例:最小可用的 Web Vitals 上报(TypeScript)
import {onCLS, onINP, onLCP} from 'web-vitals';
function sendToAnalytics(metric: {name: string; value: number; id: string}) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
url: location.href,
ts: Date.now(),
});
navigator.sendBeacon('/rum/web-vitals', body);
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
- 带上上下文:路由、设备类型、网络类型、用户 ID(脱敏)、版本号等,方便聚合分析
- 关注分位数:尤其是 P75/P90,而不是平均值
- 做好采样:避免全量上报带来额外开销
7. 优化落地流程(从发现到回归)
如果你是第一次系统性做 Web Vitals,建议按这个顺序推进:
- 先建立基线:线上 P75 的 LCP/INP/CLS 各是多少?最差的页面是谁?
- 锁定“最大收益点”:通常先从 LCP(首屏大图/TTFB)与 CLS(未占位)入手
- 用实验室工具复现:Lighthouse/DevTools 找到瓶颈点
- 做针对性优化:按上文清单逐项落地
- 回归与防劣化:把关键页面指标纳入发布门禁(至少做对比与报警)
8. 常见误区(很容易踩)
- 只看 Lighthouse,不看真实用户:实验室数据高不代表线上好
- 只优化平均值:平均值好但 P75 差,用户依然会觉得糟糕
- 把首屏图懒加载:常见但致命,会拖慢 LCP
- 把交互慢都怪“网络”:INP 很多问题在主线程(JS/渲染)而不是请求
- CLS 只在加载阶段发生:运行过程中异步插入内容同样会导致 CLS 变差
面试高频问答
Q1:Web Vitals 和 Core Web Vitals 有什么区别?
答: Web Vitals 是一组衡量用户体验的指标体系;Core Web Vitals 是其中最核心、最常用来评估页面体验的一组指标(当前主要是 LCP、INP、CLS)。
Q2:LCP 衡量的是什么?它和 FCP 有什么不同?
答:
- FCP(First Contentful Paint):首次有内容绘制(可能只是一个小图标/一行字)
- LCP:首屏“最大内容元素”绘制完成
一般来说,LCP 更接近用户对“主要内容是否出来了”的体感。
Q3:LCP 优化时第一步应该做什么?
答: 先确认 LCP 元素是谁(大图?大标题?),再针对性优化。很多时候改了半天资源加载,结果 LCP 元素其实是另一个更大的文本块或图片。
Q4:为什么说首屏大图不要 loading="lazy"?
答: 懒加载会把图片下载推迟到更晚,首屏最大内容就更晚出现,直接拉高 LCP。正确做法是:首屏关键图优先加载,其它非首屏图片再懒加载。
Q5:INP 变差通常说明什么问题?
答: 通常说明 主线程太忙:长任务多、事件回调太重、渲染更新范围太大、第三方脚本占用等。INP 优化的核心是“让主线程更快空出来并尽快完成下一帧绘制”。
Q6:如何快速定位导致 INP 差的代码?
答: 用 DevTools Performance 录制一次“慢交互”,在时间轴里找到对应的 Interaction,查看主线程上是否有长任务(长时间的 JS 执行、样式计算、布局、绘制),再结合调用栈定位到具体函数。
Q7:CLS 是怎么计算的?为什么有时候页面“看起来跳了”但 CLS 不高?
答: CLS 关注“意外位移”,大致由影响面积比例 × 位移距离比例构成,并按会话窗口聚合。若位移发生前 500ms 内有用户输入,通常不计入 CLS,所以某些用户主动触发的展开/收起不会拉高 CLS。
Q8:导致 CLS 的最常见原因是什么?怎么修?
答: 最常见的是图片/广告位未占位。修复方法是为媒体元素写 width/height 或 aspect-ratio,为广告/异步模块预留容器高度,避免加载后把内容顶开。
Q9:线上监控 Web Vitals,为什么要看 P75 而不是平均值?
答: 平均值会被少量极快/极慢样本“拉偏”。P75 更能代表“大多数用户体验”,也更接近 Core Web Vitals 的评估方式。
Q10:如果一个页面 LCP 很差,你会按什么顺序排查?
答:
- 找到 LCP 元素(图片/文本)
- 看 TTFB 是否过高(服务端/缓存/重定向)
- 看关键资源下载是否慢(图片大小、优先级、CDN、阻塞资源)
- 看渲染是否被阻塞(同步 JS、主线程长任务、阻塞 CSS)