Files
zhuangang/topics/frontend/fe-testing-perf/02-web-vitals.md
T

97 lines
4.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: frontend.fe-testing-perf.web-vitals
title: 前端性能:Core Web Vitals(LCP / INP / CLS)、长任务和优化清单
type: lesson
domain: frontend
stack: [fe-testing-perf]
tags: [性能优化, Core Web Vitals, LCP, INP, CLS, 长任务, 监控]
level: intermediate
prerequisites: [frontend.ssr.rendering-modes]
est_hours: 2.5
related: [frontend.fe-testing-perf.scenario.perf-regression, frontend.webpack.scenario.build-speed]
week: [7]
order: 2
---
# 前端性能:Core Web Vitals(LCP / INP / CLS)、长任务和优化清单
> 指标定义和阈值以官方文档为准:https://web.dev/articles/vitals 。
## 解释说明
### Core Web Vitals
| 指标 | 含义 | “良好”阈值(按 75 分位统计) | 常见原因 |
| --- | --- | --- | --- |
| **LCP**(最大内容绘制) | 视口中最大的图片 / 文本块渲染完成的时间 | ≤ 2.5 秒 | 服务器慢、渲染阻塞资源、首屏大图没优化、客户端渲染 |
| **INP**(交互到下一次绘制) | 用户交互到页面视觉响应的延迟,取整个访问期间较差的值 | ≤ 200 毫秒 | 主线程长任务、事件处理太重、大范围重新渲染 |
| **CLS**(累计布局偏移) | 页面元素意外移动的程度 | ≤ 0.1 | 图片 / 广告没有预留尺寸、字体切换、动态插入内容 |
INP 在 2024 年取代了 FID。
### 实验室数据和真实用户数据
- **实验室数据**:Lighthouse、浏览器性能面板,可复现,适合调试和 CI 回归。
- **真实用户数据(RUM)**:在线上用 `web-vitals` 库采集上报,反映真实设备和网络;以 75 分位判断。
### 长任务
主线程上超过 50 毫秒的任务。期间无法响应点击,导致 INP 变差。办法:把计算切块并让出主线程(`scheduler.yield()` 或 `setTimeout`)、移到 Web Worker、减少重新渲染的范围、虚拟列表。
### 优化清单
- **LCP**:SSR / SSG、关键资源预加载(`<link rel="preload">`)、图片压缩和现代格式、首屏图不要懒加载、CDN。
- **INP**:拆分长任务、防抖节流、React 的 `useTransition` / `useDeferredValue`、虚拟列表。
- **CLS**:图片和视频写宽高或 `aspect-ratio`、为动态内容预留空间、字体 `font-display`。
- **通用**:减少 JS 体积(懒加载、按需引入)、HTTP 缓存、压缩。
## 案例
AI 对话页面,回答很长时流式追加 Markdown 并实时高亮代码,输入框打字卡顿(INP 变差):每收到一个 token 就重新解析整段 Markdown。改为按时间批量合并 token(如每 50ms 渲染一次)、只重新解析最后一个段落、代码高亮放到 Worker。
## 代码示例
把大量处理切块、让出主线程([examples/search-box/src/chunked.ts](examples/search-box/src/chunked.ts),`node src/chunked.ts` 自检通过):
```ts
export async function processInChunks<T, R>(items: readonly T[], fn: (item: T) => R,
{ budgetMs = 8, yieldFn = defaultYield, now = () => performance.now() } = {}) {
const results: R[] = [];
let start = now();
for (const item of items) {
results.push(fn(item));
if (now() - start >= budgetMs) { // 本块用完预算,让浏览器先处理交互和渲染
await yieldFn();
start = now();
}
}
return results;
}
```
线上采集(示意,API 以 `web-vitals` 库文档为准):
```ts
import { onCLS, onINP, onLCP } from "web-vitals";
function report(metric: { name: string; value: number; id: string }) {
navigator.sendBeacon("/api/rum", JSON.stringify({ ...metric, page: location.pathname }));
}
onLCP(report);
onINP(report);
onCLS(report);
```
## 易错点
- 只看本地 Lighthouse 分数,不看真实用户的数据。
- 把首屏大图也懒加载了,LCP 反而变差。
- 用平均值而不是分位数衡量。
- 优化完没有监控和预算,几个版本后退化。
## 面试常问
- 说说 LCP、INP、CLS 和各自的优化方法。
- 页面交互卡顿怎么排查?
- 你做过的性能优化,数据是多少?怎么测的?