性能
发布于
完成本指南后,你的 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; 本指南是改善它们的流程。
用 web-vitals 采集真实用户数据
Section titled “用 web-vitals 采集真实用户数据”实验室工具看不到真实用户在自己设备上的体验,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 百分位。
用 Lighthouse 复现慢页面
Section titled “用 Lighthouse 复现慢页面”打开 Chrome DevTools,选择 Lighthouse 面板,对真实用户数据标出的生产 URL 跑一次移动端审计。 Lighthouse 能测 LCP 与 CLS,但测不了 INP:实验室运行只加载页面、不交互,所以用 Total Blocking Time 作为代理。 Lighthouse 对桌面端的评分也比线上阈值更严(桌面端 LCP 低于 1.2 s 才是绿色,移动端是 2.5 s), 所以实验室数字只和实验室数字比。
压低 LCP
Section titled “压低 LCP”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
Section titled “压低 INP”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));}压低 CLS
Section titled “压低 CLS”为一切迟到的内容预留空间:图片与视频写上 width 与 height 属性,嵌入内容用 aspect-ratio,
广告与同意横幅放在固定高度的容器里,流式加载的内容设置 min-height。动画用 transform,
不用 top/left,前者不计入布局偏移。
为重复启动预缓存外壳
Section titled “为重复启动预缓存外壳”PWA 的结构性优势在于已安装的启动:Service Worker 预缓存的外壳无需网络往返即可绘制。按 离线策略预缓存 HTML、CSS 与 JavaScript,然后把第二次启动的 LCP 与首次访问分开测量; 首次访问仍要付全价。
用 Lighthouse CI 守住成果
Section titled “用 Lighthouse CI 守住成果”加一道预算,让回退在构建阶段失败,而不是等下一份线上报告。Lighthouse CI 对静态构建或已启动的服务器运行 Lighthouse,并对审计项做断言。
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
- 启动性能:绘制与导航计时
- 离线策略
- Web Vitals(web.dev)
- Interaction to Next Paint (INP)(web.dev)
- web-vitals(github.com)
← 返回指南总览。