性能优化概述与指标
优化的第一步不是「上手段」,而是知道现在几分、哪个指标差、差在哪个元素上。 这篇讲三件事:指标是什么、怎么采到真实数据、指标差了去哪里查。
一、指标是「用户体验」的代理
用户只关心三件事:能不能快点看到内容、点了有没有反应、页面会不会乱跳。对应的就是三个 Core Web Vitals。
二、Core Web Vitals(现行版本)
| 指标 | 全称 | 衡量什么 | 良好 | 需改进 | 差 |
|---|---|---|---|---|---|
| LCP | Largest Contentful Paint | 最大内容块绘制完成的时间(「看到主要内容」) | ≤ 2.5s | 2.5–4s | > 4s |
| INP | Interaction to Next Paint | 交互到下一次绘制完成(「点了多久有反应」) | ≤ 200ms | 200–500ms | > 500ms |
| CLS | Cumulative Layout Shift | 累积布局偏移(「页面乱跳」) | ≤ 0.1 | 0.1–0.25 | > 0.25 |
⚠️ FID 已被 INP 取代
| FID(旧) | INP(新) | |
|---|---|---|
| 测什么 | 首次输入的延迟(只到开始处理为止) | 所有交互从输入到绘制完成的耗时 |
| 什么时候用 | 2024 年 3 月前 | 现在(已进入 CWV) |
| 为什么淘汰 | 只测第一次、且不含处理与渲染时间,容易「看起来很好」 | 覆盖完整链路,更贴近真实体感 |
面试时如果还说 FID 是核心指标,会显得没跟上;正确说法是「FID 已被 INP 取代」。
辅助/诊断指标
| 指标 | 说明 | 用途 |
|---|---|---|
| TTFB | 首字节时间(网络 + 服务端) | 定位是不是后端/网络慢 |
| FCP | 首次内容绘制 | 白屏结束得早不早 |
| TBT | 总阻塞时间(主线程被长任务占用的总和) | 实验室里预测 INP |
| SI | Speed Index,视觉完成速度 | 整体观感 |
| TTI | 可交互时间 | 老指标,仍可用于实验室分析 |
三、两种数据:实验室 vs 真实用户
| 实验室(Lab) | 真实用户(RUM) | |
|---|---|---|
| 来源 | Lighthouse / DevTools / WebPageTest | 用户浏览器上报 |
| 优点 | 可复现、能定位到具体代码 | 反映真实设备、网络、地域 |
| 缺点 | 环境单一(常在快网络+好机器上跑) | 有采样与上报误差 |
| 用途 | 排查和验证优化 | 衡量优化是否真的生效 |
结论:用 Lab 定位、用 RUM 验收。只看 Lighthouse 分数优化,容易优化到「跑分」而不是「体验」。
四、怎么采集(可直接用)
js
// vitals.js —— 用 web-vitals v4(onFID 已移除)
import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals'
function report(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
id: metric.id,
url: location.pathname,
// 建议再带上:设备类型、网络类型、版本号
})
// sendBeacon:页面关闭/跳转时也能发出,不阻塞卸载
navigator.sendBeacon('/api/vitals', body)
}
onLCP(report)
onINP(report)
onCLS(report)
onTTFB(report)
onFCP(report)// vitals.js —— 用 web-vitals v4(onFID 已移除)
import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals'
function report(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
id: metric.id,
url: location.pathname,
// 建议再带上:设备类型、网络类型、版本号
})
// sendBeacon:页面关闭/跳转时也能发出,不阻塞卸载
navigator.sendBeacon('/api/vitals', body)
}
onLCP(report)
onINP(report)
onCLS(report)
onTTFB(report)
onFCP(report)手写 PerformanceObserver(不引库)
js
new PerformanceObserver((list) => {
const entries = list.getEntries()
const last = entries[entries.length - 1] // LCP 会持续更新,取最后一条
console.log('LCP', last.startTime, last.element) // element 就是罪魁祸首
}).observe({ type: 'largest-contentful-paint', buffered: true })
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
if (!e.hadRecentInput) console.log('CLS 贡献', e.value, e.sources)
}
}).observe({ type: 'layout-shift', buffered: true })new PerformanceObserver((list) => {
const entries = list.getEntries()
const last = entries[entries.length - 1] // LCP 会持续更新,取最后一条
console.log('LCP', last.startTime, last.element) // element 就是罪魁祸首
}).observe({ type: 'largest-contentful-paint', buffered: true })
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
if (!e.hadRecentInput) console.log('CLS 贡献', e.value, e.sources)
}
}).observe({ type: 'layout-shift', buffered: true })上报设计要点:
- 用
sendBeacon(普通fetch在页面卸载时会被取消); - 采样(如 10% 用户),否则数据量爆炸;
- 带上版本号与页面路径,否则优化前后无法对比;
- 取 P75(第 75 百分位)作为结论,别看平均值。
五、指标差了,去哪查
LCP 慢
LCP 元素通常是:大图、大段文字块、视频封面。
| 排查步骤 | 怎么做 |
|---|---|
| 1. 找出是哪个元素 | DevTools Performance 面板勾选 Web Vitals,时间轴上会标 LCP;或看 largest-contentful-paint entry 的 element |
| 2. 拆分耗时 | 看 LCP 元素请求的四个阶段:TTFB → 资源加载 → 资源加载完到渲染 → 渲染延迟 |
| 3. 对症 | TTFB 慢 → 后端/CDN;资源大 → 压缩/WebP/预加载;渲染被阻塞 → 减少同步 JS、font-display: swap |
INP 差(交互卡)
| 排查步骤 | 怎么做 |
|---|---|
| 1. 找长任务 | Performance 面板看主线程的红色长任务(>50ms) |
| 2. 看是谁占的 | 展开长任务的调用栈,找业务函数 / 第三方脚本 |
| 3. 常见原因 | 大列表渲染、同步 localStorage、复杂计算在主线程、第三方 SDK |
手段:拆分长任务(setTimeout/scheduler.yield)、虚拟列表、计算移到 Web Worker、startTransition 降优先级。
CLS 差(布局跳动)
| 原因 | 修法 |
|---|---|
| 图片/视频没写尺寸 | 显式 width/height 或 aspect-ratio |
| 广告/嵌入内容后插入 | 预留固定容器高度 |
| 字体切换导致重排 | font-display: swap + 预加载字体,或 size-adjust |
| 动态插入内容把下面顶走 | 用 transform 动画代替改尺寸,或用占位骨架 |
排查技巧:Performance 面板里 Layout Shift 事件能看到具体移了哪些元素。
六、指标 → 手段对照表
| 想改善 | 优先做什么 |
|---|---|
| LCP | 图片压缩/WebP、fetchpriority="high"、CDN、减少渲染阻塞资源、SSR/SSG |
| INP | 拆长任务、虚拟列表、Web Worker、减少第三方脚本 |
| CLS | 给媒体写尺寸、字体优化、预留广告位 |
| TTFB | 服务端缓存、CDN、减少重定向、数据库慢查询 |
七、RAIL 模型(交互设计的时间预算)
| 阶段 | 目标 | 用户感知 |
|---|---|---|
| Response | 响应 < 100ms | 点击即时反馈 |
| Animation | 每帧 < 16ms(60fps) | 动画流畅 |
| Idle | 利用空闲时间 < 50ms 分片 | 不阻塞主线程 |
| Load | 首屏 < 1s(理想) | 快速呈现 |
八、速查
| 问题 | 答案 |
|---|---|
| 三个核心指标 | LCP、INP、CLS |
| FID 还能用吗 | 不能,已被 INP 取代(2024) |
| INP 良好阈值 | ≤ 200ms |
| CLS 良好阈值 | ≤ 0.1 |
| Lab 和 RUM 怎么配合 | Lab 定位、RUM 验收 |
| 上报用什么发 | navigator.sendBeacon |
| 看什么分位 | P75,不是平均值 |
| LCP 慢先看什么 | LCP 元素是哪张图/哪块文字,再拆四段耗时 |
Cao Note