「网站感觉慢」是所有性能问题的起点,但它不可度量也无法修复。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] → [样式/布局/绘制] → 下一帧
↑ 交互延迟的主要来源 ↑ 过长也会拖慢
常见成因按频率排序:
- 长任务(>50ms 的同步 JS)阻塞主线程,让「下一个交互」排队;
- 交互处理器做太多:一次点击里同步完成计算、DOM 更新、第三方调用;
- 强制同步布局:读写交错(读
offsetHeight后立即写样式)反复触发布局; - 大 DOM:节点数越多,样式重算越慢。
修复三板斧:
- 长任务拆分:
scheduler.yield()/setTimeout把非紧急工作让出主线程; - 输入即反馈:交互处理器先做乐观 UI(立即高亮/展开),重活放
requestAnimationFrame之后或 Web Worker; - 减少第三方脚本:统计/聊天/广告脚本是不受你控制的长任务来源,按需加载。
CLS:意外偏移的三个来源
CLS 累计「无用户输入窗口内」的布局偏移分数。三个最常见的来源,按修复性价比排序:
- 无尺寸的图片/视频:
<img>没有width/height(或 CSS aspect-ratio),加载后把下方内容顶开——一行修复,收益最大; - 动态注入内容:广告位、Cookie 横幅、无限滚动把已有内容推走——预留固定高度或用
min-height; - 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 性能优化完全指南;本文的测量命令可直接对任意站点复跑。