跳转到内容

性能

发布于

完成本指南后,你的 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; 本指南是改善它们的流程。

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

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 百分位。

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

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

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

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));
}

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

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

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

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 步的线上数据会在几天的流量内 确认改动对真实用户生效。

← 返回指南总览。