← 返回博客首页

Core Web Vitals 详解:LCP、INP、CLS 的测量与优化清单

「网站感觉慢」是所有性能问题的起点,但它不可度量也无法修复。Google 把「感觉慢」拆成三个可量化的指标——LCP(最大内容绘制,加载速度)、INP(交互到下一次绘制,响应速度)、CLS(累积布局偏移,视觉稳定性)——并给每个指标划了及格线:2.5 秒 / 200 毫秒 / 0.1。

这篇的重点不是复述定义,而是三件更容易出错的事:实验室数据与真实用户数据的口径差异、每个指标的常见成因排序、以及一条可以直接照跑的测量命令(用本站生产环境实测演示)。


三个指标,三种测量口径

指标 度量什么 及格线(P75) 常见破坏者
LCP 最大内容元素渲染完成 ≤ 2.5 s 慢 TTFB、未优化的首图、阻塞字体
INP 全部交互的输入到下一次绘制 ≤ 200 ms 长任务、同步布局、大量 DOM
CLS 意外布局偏移的累计 ≤ 0.1 无尺寸的图片/广告、动态注入内容

三者取的是 P75 分位(75% 的访问达标)而非平均值——平均数会被好设备拉高,掩盖差用户的体验。

实验室(Lab)与真实用户(Field)的分工

Lab(Lighthouse / DevTools) Field(CrUX / web-vitals 库)
环境 固定设备/网络/位置,可复现 真实设备/网络,不可复现
采样 单次 28 天聚合
指标 全部(含 LCP/CLS) LCP/INP/CLS/FCP/TTFB
用途 定位问题(有调用栈/瀑布图) 确认影响(真实分布)
陷阱 设备太好,INP 偏乐观 样本量小时无细分数据

正确流程:Field 数据发现「INP 的 P75 超标」→ Lab 里复现该交互 → 定位长任务 → 修复 → Lab 验证 → 等 CrUX 刷新(约 28 天滚动)确认。只跑 Lighthouse 就宣布优化完成,是用「最容易的设备」给「最差的场景」打分。


LCP:从 TTFB 到渲染的完整链路

LCP 是视口内最大的内容元素(图片/视频/大块文本)渲染完成的时刻。它不是单一瓶颈,而是四个阶段的串联,瓶颈在哪个阶段就修哪里:

阶段 占比(典型) 优化手段
① TTFB — CDN、缓存策略、边缘渲染
② 资源发现延迟 常见 <link rel="preload"> 首图与关键 CSS
③ 资源加载时长 常见 首图压缩、fetchpriority='high'、现代格式
④ 渲染延迟 较少 避免首屏 JS 阻塞、字体 font-display: swap

实测:本站生产环境四个页面的 TTFB(各 3 次取中位)——

curl -s -o /dev/null -w '%{time_starttransfer}\n' https://oltool.net/json-formatter.html
# 三次取中位 → 0.075s
页面 TTFB(中位) 总耗时
/(首页) 0.064 s 0.135 s
/json-formatter.html 0.075 s 0.105 s
/blog/json-parse-errors.html 0.073 s 0.084 s
/collections/convert-excel-to-csv.html 0.083 s 0.096 s

TTFB 在 64–83 毫秒区间(EdgeOne 边缘节点),SSR 首字节远低于 800 毫秒的警戒线 ⇒ LCP 的瓶颈不在服务器,剩余预算应花在首图与字体上。


INP:不只是「点击要快」

INP 统计页面整个生命周期内所有交互(点击/键盘/触摸)从输入到下一次绘制的延迟,取近似最差值。交互延迟的构成:

输入 → [事件处理 JS] → [样式/布局/绘制] → 下一帧
       ↑ 交互延迟的主要来源        ↑ 过长也会拖慢

常见成因按频率排序:

  1. 长任务(>50ms 的同步 JS)阻塞主线程,让「下一个交互」排队;
  2. 交互处理器做太多:一次点击里同步完成计算、DOM 更新、第三方调用;
  3. 强制同步布局:读写交错(读 offsetHeight 后立即写样式)反复触发布局;
  4. 大 DOM:节点数越多,样式重算越慢。

修复三板斧:

  • 长任务拆分:scheduler.yield() / setTimeout 把非紧急工作让出主线程;
  • 输入即反馈:交互处理器先做乐观 UI(立即高亮/展开),重活放 requestAnimationFrame 之后或 Web Worker;
  • 减少第三方脚本:统计/聊天/广告脚本是不受你控制的长任务来源,按需加载。

CLS:意外偏移的三个来源

CLS 累计「无用户输入窗口内」的布局偏移分数。三个最常见的来源,按修复性价比排序:

  1. 无尺寸的图片/视频:<img> 没有 width/height(或 CSS aspect-ratio),加载后把下方内容顶开——一行修复,收益最大;
  2. 动态注入内容:广告位、Cookie 横幅、无限滚动把已有内容推走——预留固定高度或用 min-height;
  3. Web 字体替换(FOUT):字体加载后文本重排——size-adjust / font-display: optional 缓解。

动画用 transform(不触发布局)而不是 top/left(触发)——这也是「CLS = 0 但动画不流畅」时该查的方向:流畅性是帧率问题,归 INP 的响应链路。


可复现的实测结果

生产 TTFB(各 3 次取中位,--noproxy 直连):

for i in 1 2 3; do
  curl -s -o /dev/null -w '%{time_starttransfer}\n' https://oltool.net/json-formatter.html
done | sort -n | sed -n 2p
# → 0.075

Performance API 全链路(DevTools Console 直接粘贴即可,任意页面):

const t = performance.getEntriesByType('navigation')[0];
const fcp = performance.getEntriesByType('paint').at(-1)?.startTime ?? 0;
console.log({
  ttfb: Math.round(t.responseStart - t.startTime),
  fcp: Math.round(fcp),
  domContentLoaded: Math.round(t.domContentLoadedEventEnd - t.startTime),
  load: Math.round(t.loadEventEnd - t.startTime),
});
// 本站生产实测:{ ttfb: 64, fcp: ?, domContentLoaded: ?, load: ? }(TTFB 远低于 800ms 警戒线)

采集 CLS/INP 需要 web-vitals 库(Performance API 不直接暴露 INP):

import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log); onINP(console.log); onCLS(console.log);
// 上报到你的分析端,聚合出 P75 才是 CrUX 同口径的数据

优化清单(按收益排序)

# 动作 主要改善 成本
1 图片加 width/height 或 aspect-ratio CLS 一行/图
2 首图 fetchpriority='high' + preload LCP 一行
3 非关键 JS 加 defer / 按需加载 INP / LCP 低
4 字体 font-display: swap + 预加载 LCP / CLS 低
5 长任务用 scheduler.yield() 拆分 INP 中
6 TTFB:CDN / 缓存 / 边缘渲染 LCP 视架构
7 图片转 WebP/AVIF 并压缩 LCP 中

小结

  • 先测量后优化:Field 定位影响面,Lab 定位成因,顺序反了会优化空气;
  • 三个指标三种口径:LCP 看资源链路、INP 看主线程、CLS 看布局预留;
  • P75 是真实标准:平均值与中位数都会掩盖差用户体验;
  • 本站生产实测:TTFB 64–83ms、各页总耗时 <150ms(边缘渲染 + CDN)——性能基线健康,剩余优化空间在前端资源层。

系统性的性能优化全景见 Web 性能优化完全指南;本文的测量命令可直接对任意站点复跑。


广告

常见问题

Lighthouse 满分,为什么真实用户还是抱怨慢?

因为 Lighthouse 是**实验室数据**:固定的设备、网络与位置,单次采样。真实用户的 INP/LCP 来自**CrUX 真实用户数据**——设备更差、网络更杂、样本量更大。两者口径不同,Lab 用于定位问题(可复现),Field 用于确认影响面(真实分布)。优化方向以 Field 数据的差用户(P75)为准。

TTFB 很快,LCP 还是慢,问题在哪?

TTFB 只覆盖「请求发出到首字节」,LCP 是「最大内容元素渲染完成」。中间还有:HTML 下载与解析、关键资源(CSS/字体/首图)的发现与加载、渲染排队。常见瓶颈是**首图未做优先级提示**(缺 fetchpriority='high')、**字体阻塞**或 **CSS 过大**。逐段拆解方法见正文。

CLS 是 0 为什么还会闪动?

CLS 只统计**没有用户输入的 1500 毫秒窗口内**的布局偏移,且动画(transform)不计入。如果闪动发生在用户输入后的窗口里(比如点击展开),它不算 CLS 但用户仍会感知。反过来,CLS = 0 只说明「没有意外偏移」,不代表动画流畅(那是帧率问题,归 INP 的响应延迟)。

INP 替代 FID 后,我的 FID 数据还有用吗?

没用,FID 已于 2024 年 3 月被 INP 取代。两者口径不同:FID 只测**第一次**交互的输入延迟,INP 统计**整个生命周期内所有交互**的近似最差值。FID 数据好不代表 INP 达标——页面后面的交互往往比第一次更慢。迁移方法:用 web-vitals 库上报 INP,并把「交互后的处理」拆进异步任务。

← 返回博客首页