跳到主要内容

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.5s2.5s ~ 4.0s> 4.0s
INP交互体验点击/输入后多久“有反应”≤ 200ms200ms ~ 500ms> 500ms
CLS视觉稳定性页面会不会“抖动/位移”≤ 0.10.1 ~ 0.25> 0.25
记忆法
  • LCP:Largest → “最大的那块内容”出来的时间
  • INP:Interaction → “交互后”到“下一次绘制”
  • CLS:Layout Shift → 页面“布局偏移”的累计程度

2. 先搞清楚:实验室数据 vs 真实用户数据

很多同学会遇到这种情况:Lighthouse 跑分很好,但线上用户仍然觉得卡;或者本地复现不了线上问题。原因通常是数据来源不同

  • 实验室数据(Lab):在可控环境里跑出来的指标(如 Lighthouse),优点是可复现、方便定位;缺点是不等于真实用户
  • 真实用户数据(Field / RUM):来自真实用户设备与网络的指标(如你自己做的 RUM 埋点、或者使用 CrUX 数据),优点是最贴近真实体验;缺点是噪音多、定位要配合更多上下文
为什么强调 75 分位?

Core Web Vitals 通常关注“大多数用户的体验”,不是平均值。实践中经常用 P75(75th percentile) 来判断页面是否达标:
如果 P75 仍然不好,说明至少 25% 的用户体验很糟糕,需要优先处理。

3. LCP:最大内容绘制(Largest Contentful Paint)

3.1 LCP 在衡量什么?

LCP 记录的是:从用户开始打开页面,到视口内最大的内容元素完成渲染的时间。

这个“最大内容”通常是:

  • 首屏大图(Hero Image)
  • 首屏最大的文本块(大标题/大段文字)

它回答的问题是:“页面什么时候看起来‘主要内容已经出来了’?”

LCP 不等于“页面完全加载”

LCP 只关注“首屏最大内容绘制完成”,并不要求所有资源加载完(比如评论区、推荐列表、埋点脚本都可能还在加载)。

3.2 LCP 由哪些部分组成?

把 LCP 理解成一条链路会更好排查(常见情况是某一段拖后腿):

实践里,你可以把 LCP 优化拆成三类:

  1. 后端/网络慢(TTFB 高)
  2. 资源下载慢(关键 CSS/JS/图片慢)
  3. 渲染慢(主线程忙、渲染阻塞)

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 记录的是:

一次交互从发生开始,到页面完成“下一次绘制”(用户能看到反馈)所花的时间。

它回答的问题是:“用户操作以后,页面多久才真正‘有反应’?”

INP 为什么比 FID 更实用?

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)”,取其中最大的一段作为最终值(避免把整页生命周期所有偏移无限累加)。

哪些位移不算 CLS?

如果某次位移发生前 500ms 内有用户输入(recent input),通常不计入 CLS。
也就是说:用户主动触发的布局变化(比如展开手风琴)不应算“意外位移”。

5.3 常见导致 CLS 变差的原因

  • 图片/视频没有尺寸,加载后把内容“顶开”
  • 广告/卡片异步插入,没有提前占位
  • Web 字体加载后替换字体,导致文字尺寸变化
  • 首屏上方插入 banner、提示条(尤其是接口返回后才决定展示)
  • 动画使用 top/left/height 等会影响布局的属性

5.4 CLS 优化清单(“提前占位 + 避免顶格插入”)

1)所有媒体元素都要“可预测尺寸”

  • <img>width/height(或使用 CSS aspect-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,建议按这个顺序推进:

  1. 先建立基线:线上 P75 的 LCP/INP/CLS 各是多少?最差的页面是谁?
  2. 锁定“最大收益点”:通常先从 LCP(首屏大图/TTFB)与 CLS(未占位)入手
  3. 用实验室工具复现:Lighthouse/DevTools 找到瓶颈点
  4. 做针对性优化:按上文清单逐项落地
  5. 回归与防劣化:把关键页面指标纳入发布门禁(至少做对比与报警)

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/heightaspect-ratio,为广告/异步模块预留容器高度,避免加载后把内容顶开。

Q9:线上监控 Web Vitals,为什么要看 P75 而不是平均值?

答: 平均值会被少量极快/极慢样本“拉偏”。P75 更能代表“大多数用户体验”,也更接近 Core Web Vitals 的评估方式。

Q10:如果一个页面 LCP 很差,你会按什么顺序排查?

答:

  1. 找到 LCP 元素(图片/文本)
  2. 看 TTFB 是否过高(服务端/缓存/重定向)
  3. 看关键资源下载是否慢(图片大小、优先级、CDN、阻塞资源)
  4. 看渲染是否被阻塞(同步 JS、主线程长任务、阻塞 CSS)