# 性能

> 在真实用户中测量 LCP、INP 与 CLS，用 Lighthouse 复现慢页面，逐项修复，预缓存外壳，再用 Lighthouse CI 守住成果。

完成本指南后，你的 PWA 在真实用户数据中达到 Core Web Vitals 阈值（LCP 2.5 s、INP 200 ms、CLS 0.1，
均取页面加载的第 75 百分位），重复启动时从缓存的外壳绘制，并且有一个 CI 步骤会在指标回退时让构建失败。
性能工作是一个循环：测量，修复最大的问题，再测量。

你需要通过 HTTPS 提供的生产构建、一台中端手机或 Chrome 的 CPU 与网络节流，以及一个接收遥测数据的地方。
指标定义见 [PWA 的 Core Web Vitals：LCP、INP 与 CLS](/zh/reference/performance/core-web-vitals/)；
本指南是改善它们的流程。

## 用 web-vitals 采集真实用户数据

实验室工具看不到真实用户在自己设备上的体验，Chrome User Experience Report 给的是源级别的数字、没有逐页诊断，
所以要在页面里埋点。`web-vitals` 库（Apache 2.0）封装了底层浏览器 API，在每个指标定稿时上报；`onCLS` 与
`onINP` 可能触发不止一次，例如页面被隐藏时。

```js
import { onCLS, onINP, onLCP } from 'web-vitals/attribution';

function report(metric) {
  const body = JSON.stringify({
    name: metric.name, // 'LCP' | 'INP' | 'CLS'
    value: metric.value,
    rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
    id: metric.id,
    target: metric.attribution?.element ?? metric.attribution?.interactionTarget ?? null,
    path: location.pathname,
  });
  (navigator.sendBeacon && navigator.sendBeacon('/vitals', body)) ||
    fetch('/vitals', { body, method: 'POST', keepalive: true });
}

onCLS(report);
onINP(report);
onLCP(report);
```

`attribution` 构建比标准构建多约 1.5 KB（Brotli），但能告诉你哪个元素是 LCP 候选、哪次交互最慢，
这是「一个数字」与「一个修复」的区别。按路径聚合，然后分移动端与桌面端读第 75 百分位。

## 用 Lighthouse 复现慢页面

打开 Chrome DevTools，选择 **Lighthouse** 面板，对真实用户数据标出的生产 URL 跑一次移动端审计。
Lighthouse 能测 LCP 与 CLS，但测不了 INP：实验室运行只加载页面、不交互，所以用 Total Blocking Time 作为代理。
Lighthouse 对桌面端的评分也比线上阈值更严（桌面端 LCP 低于 1.2 s 才是绿色，移动端是 2.5 s），
所以实验室数字只和实验室数字比。

:::observed
Chrome DevTools 的 Lighthouse 移动端报告在 Performance 下列出 **Largest Contentful Paint**，并有一项
**Largest Contentful Paint element** 诊断（英文界面），指出对应的 DOM 节点，并把耗时拆成 TTFB、加载延迟、
加载时间与渲染延迟。该审计文档给出的 LCP 分段为：移动端 0 到 2.5 s（绿）、2.5 到 4 s（橙）、超过 4 s（红）；
桌面端 0 到 1.2 s、1.2 到 2.4 s、超过 2.4 s。
:::

## 压低 LCP

LCP 元素通常是主图，或者一段等待 Web 字体的文本。给图片加上 `fetchpriority="high"` 与
`<link rel="preload">`，按渲染尺寸提供 AVIF 或 WebP，并把阻塞渲染的样式表与脚本移出它的路径。
字体方面：`font-display: swap` 会先用回退字体立即绘制文字，代价是可见的字体切换；`optional` 避免切换，
代价是有时根本不显示 Web 字体。

```html
<link rel="preload" as="image" href="/hero-1200.avif" imagesrcset="/hero-800.avif 800w, /hero-1200.avif 1200w" imagesizes="100vw">
<img src="/hero-1200.avif" srcset="/hero-800.avif 800w, /hero-1200.avif 1200w" sizes="100vw" width="1200" height="600" fetchpriority="high" alt="">
```

## 压低 INP

INP 是页面整个生命周期中最慢的一次点击、点按或按键的延迟（每 50 次交互丢弃一个离群值），
从输入开始计到下一帧绘制。主线程上的长任务是常见原因。有 `scheduler.yield()` 时用它拆分工作，没有时用
`setTimeout(resolve, 0)`，延迟非关键 JavaScript，并让处理函数自身的工作尽量小，好让下一帧在昂贵部分
运行之前就能绘制。

```js
async function onFilterChange(event) {
  renderSpinner(); // 便宜：先绘制反馈
  await yieldToMain();
  applyFilter(event.target.value); // 昂贵：在这一帧之后运行
}

function yieldToMain() {
  if ('scheduler' in globalThis && 'yield' in scheduler) return scheduler.yield();
  return new Promise((resolve) => setTimeout(resolve, 0));
}
```

## 压低 CLS

为一切迟到的内容预留空间：图片与视频写上 `width` 与 `height` 属性，嵌入内容用 `aspect-ratio`，
广告与同意横幅放在固定高度的容器里，流式加载的内容设置 `min-height`。动画用 `transform`，
不用 `top`/`left`，前者不计入布局偏移。

## 为重复启动预缓存外壳

PWA 的结构性优势在于已安装的启动：Service Worker 预缓存的外壳无需网络往返即可绘制。按
[离线策略](/zh/guides/offline/)预缓存 HTML、CSS 与 JavaScript，然后把第二次启动的 LCP 与首次访问分开测量；
首次访问仍要付全价。

## 用 Lighthouse CI 守住成果

加一道预算，让回退在构建阶段失败，而不是等下一份线上报告。Lighthouse CI 对静态构建或已启动的服务器运行
Lighthouse，并对审计项做断言。

```js
// lighthouserc.js
module.exports = {
  ci: {
    collect: { staticDistDir: './dist' },
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
      },
    },
    upload: { target: 'temporary-public-storage' },
  },
};
```

用 `npm install -g @lhci/cli` 安装，在 CI 中运行 `lhci autorun`。临时公共存储是最简单的上传目标；
那里的报告公开且会过期，私有项目请换成 LHCI 服务器。这一步通过之后，第 1 步的线上数据会在几天的流量内
确认改动对真实用户生效。

## 另请参阅

- [PWA 的 Core Web Vitals：LCP、INP 与 CLS](/zh/reference/performance/core-web-vitals/)
- [启动性能：绘制与导航计时](/zh/reference/performance/startup-performance/)
- [离线策略](/zh/guides/offline/)
- [Web Vitals](https://web.dev/articles/vitals)（web.dev）
- [Interaction to Next Paint (INP)](https://web.dev/articles/inp)（web.dev）
- [web-vitals](https://github.com/GoogleChrome/web-vitals)（github.com）

← 返回[指南](/zh/guides/)总览。