97 lines
4.0 KiB
Markdown
97 lines
4.0 KiB
Markdown
---
|
||
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 和各自的优化方法。
|
||
- 页面交互卡顿怎么排查?
|
||
- 你做过的性能优化,数据是多少?怎么测的?
|