性能测试与回归治理
问题
如何建立从真实用户发现问题、实验室复现、CI 阻止回归到上线验证的完整性能工程流程?
性能测试要组合三类证据:
- 现场数据 RUM/CrUX 回答“真实用户是否慢、哪些页面和设备慢”。
- 实验室测试 回答“为什么慢”,用固定设备、网络、缓存和用户流程录制可复现的 trace。
- CI 回归测试 回答“这次改动是否让关键页面退化”,使用锁定版本、多次运行、单项指标与资源预算。
完整闭环是:RUM 分群 → 选代表样本 → 实验室复现 → 找到 trace 证据 → 小步优化 → 相同条件复测 → 灰度上线 → 按版本比较现场 P75 和业务指标。不要只追 Lighthouse 100 分,也不要用一次运行就阻塞 PR。
一、现场数据与实验室数据
| 数据 | 优点 | 局限 | 主要用途 |
|---|---|---|---|
| RUM | 真实设备、网络、内容和用户行为 | 环境复杂,定位成本高 | 判断影响面、分群、验证上线收益 |
| CrUX | 无需自建即可看公开站点真实 CWV | 维度和样本受限,约 28 天窗口 | 查看 URL/origin 长期趋势 |
| Lighthouse | 自动化、可重复、建议丰富 | 以加载审计为主,受环境噪声影响 | 快速体检、CI 基线 |
| DevTools Performance | trace 细节丰富,可看交互和渲染 | 需要可复现路径和人工分析 | 根因定位 |
| WebPageTest | 多地点、设备、视频和瀑布能力强 | 运行成本和排队时间较高 | 网络/首屏深度分析 |
实验室可能使用固定中端设备和冷缓存,现场包含高低端设备、热缓存、登录态、A/B 实验和长会话。正确做法不是让两者数字完全相同,而是让实验室场景能稳定复现现场某个受影响用户群。
二、先定义要保护的用户旅程
只测首页会漏掉大量真实问题。测试计划至少覆盖:
- 首次访问:首页/落地页、冷缓存、未登录。
- 返回访问:热缓存、BFCache 恢复、已登录。
- 核心导航:首页 → 列表 → 详情,包含 SPA 软导航。
- 核心交互:搜索输入、筛选、打开弹窗、提交表单、切换 Tab。
- 长会话:多次路由切换后内存、监听器和缓存是否增长。
- 异常环境:慢接口、资源失败、离线恢复和低端设备。
每条旅程定义:起点、数据状态、操作步骤、可见完成条件、性能指标和业务成功条件。
interface PerformanceJourney {
name: string;
route: string;
cacheState: 'cold' | 'warm';
deviceProfile: 'mobile-mid' | 'desktop-mid';
steps: string[];
budgets: {
lcp?: number;
inp?: number;
cls?: number;
transferredJs?: number;
};
}
三、测试矩阵怎么设计?
不要穷举所有组合,应从 RUM 设备分布和业务价值选择代表矩阵。
| 维度 | 至少保留的场景 |
|---|---|
| 设备 | 中端移动设备、常用桌面环境 |
| CPU | 无限制 + 与目标移动设备接近的 CPU slowdown |
| 网络 | 稳定宽带、普通 4G、必要时高延迟/丢包 |
| 缓存 | 冷缓存、热缓存、BFCache/预渲染单独验证 |
| 页面 | 公开内容页、登录后核心页、重交互页 |
| 数据 | 小数据、典型数据、接近上限的数据量 |
| 版本 | 当前生产基线、候选版本、灰度版本 |
例如,营销页可先采用“中端移动设备 + 冷缓存 + 模拟普通 4G”作为 CI 关键场景;内部管理后台可能更应保护热导航 INP 和长列表交互,而不是套用同一个 LCP 预算。
四、建立可复现基线
基线不是“团队觉得合理的数字”,而是当前生产版本在固定条件下的分布。
推荐步骤:
- 锁定 Chrome、Lighthouse、Node、依赖和测试镜像版本。
- 使用确定的测试数据、登录态、字体和 feature flags。
- 关闭浏览器扩展、系统后台任务和不受控 A/B 实验。
- 每条 URL 或旅程运行 3–7 次,保存所有原始结果。
- 使用中位运行结果做普通比较,同时保留最差值观察稳定性。
- 记录构建 SHA、运行器硬件、时间和配置,确保可以追溯。
运行次数不是越多越好。3 次适合快速 PR 反馈,5–7 次更适合高风险页面或波动较大的共享 runner;最终根据测试成本和方差决定。
五、不要只看 Lighthouse 总分
总分是多个指标经过评分曲线和权重计算后的结果,工具升级也可能改变分数。CI 更适合保护以下信号:
- LCP、TBT、CLS 等可解释的单项指标。
- 压缩后首屏 JS/CSS、图片、字体和第三方体积。
- 请求数、第三方域名数和关键请求链长度。
- 自定义业务标记,例如 editor-ready、search-result-painted。
- 关键交互的延迟和长任务数量。
{
"ci": {
"collect": {
"url": [
"http://localhost:3000/",
"http://localhost:3000/products/example"
],
"numberOfRuns": 5,
"settings": {
"preset": "desktop"
}
},
"assert": {
"assertions": {
"categories:performance": ["warn", { "minScore": 0.9 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"total-blocking-time": ["error", { "maxNumericValue": 300 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
}
}
}
}
这里的数字只是“桌面预设下关键公开页”的示例。移动端、登录页和后台页面要根据自己的基线与用户目标制定,不能把同一配置复制到所有项目。
六、绝对阈值与相对回归一起用
只用绝对阈值会放过“仍达标但持续变差”,只用相对阈值会在基线很差时继续容忍问题。
interface RegressionRule {
metric: string;
absoluteMax: number;
relativeMaxIncrease: number;
}
function evaluateRegression(
baseline: number,
candidate: number,
rule: RegressionRule,
): { passed: boolean; reasons: string[] } {
const reasons: string[] = [];
const increase = baseline === 0 ? 0 : (candidate - baseline) / baseline;
if (candidate > rule.absoluteMax) {
reasons.push(`${rule.metric} 超过绝对预算`);
}
if (increase > rule.relativeMaxIncrease) {
reasons.push(`${rule.metric} 相对基线退化 ${(increase * 100).toFixed(1)}%`);
}
return { passed: reasons.length === 0, reasons };
}
例如可先约定“LCP 不超过 2.5s,且相对生产基线不得退化超过 10%”。10% 是考虑当前 runner 方差后的示例,需要用历史运行数据确定误差带。
七、处理测试噪声
常见噪声来源:
- CI runner 与其他任务争抢 CPU。
- 字体、图片 CDN、第三方服务响应变化。
- A/B 实验、广告内容和个性化数据不同。
- 浏览器、Lighthouse 或评分模型升级。
- 测试服务器刚启动,缓存和 JIT 状态不同。
- 页面存在 WebSocket、轮询等持续网络请求。
降低噪声的方法:
- 使用专用或规格稳定的 runner。
- 锁定工具版本和测试数据。
- 等待服务健康,而不是固定 sleep 后盲测。
- 重复运行并比较中位值,不用单次最优值。
- 总分先 warning,稳定的单项预算再 error。
- 对第三方使用 mock 和真实环境两套测试:前者保护自身回归,后者评估整体体验。
如果方差突然变大,本身就可能是性能稳定性问题,例如随机长任务、缓存不稳定或第三方抖动。应记录每次原始结果和标准差/极差,而不是只保存一个中位数。
八、自定义业务性能标记
通用指标不一定知道业务何时真正可用。可以为关键节点打标:
performance.mark('search-start');
const result = await searchProducts(query);
renderResults(result);
requestAnimationFrame(() => {
performance.mark('search-result-painted');
performance.measure(
'search-to-result',
'search-start',
'search-result-painted',
);
});
注意 rAF 回调发生在下一次绘制前,并不严格等于像素已经呈现在屏幕上;需要精确呈现时可结合后续帧、Event Timing、LoAF 或浏览器测试工具。自定义指标要写清起点和终点语义,避免团队各自定义“首屏完成”。
九、RUM 发布对比
上线验证必须带构建版本和稳定分桶:
| 维度 | 推荐做法 |
|---|---|
| 版本 | 每条样本带 build id/release id |
| 分桶 | 用户稳定进入 control/candidate,避免每次访问随机切换 |
| 指标 | P50/P75/P90、差体验比例、样本量 |
| 分群 | 路由、移动/桌面、设备档位、网络、地区 |
| 业务 | 转化、任务成功率、错误率同时观察 |
| 周期 | 覆盖工作日/周末、缓存冷启动和足够样本 |
不要直接比较两个时间段的全站平均值:页面流量结构、市场活动和设备占比变化都可能造成假回归。
十、性能 SLO、预算和告警
一个可执行的目标应该包含:
- 对象:哪个用户旅程或页面模板。
- 指标:LCP/INP/CLS、自定义完成时间、资源体积。
- 人群:移动端/桌面端、地区、设备档位。
- 统计口径:P75、差体验比例、最小样本量和时间窗口。
- 责任人:哪个团队处理、多久响应。
- 例外:豁免原因、到期时间和偿还计划。
journey: product-detail
segment: mobile
window: 7d
minimumSamples: 1000
objectives:
lcpP75Ms: 2500
inpP75Ms: 200
clsP75: 0.1
owner: storefront-team
这里的阈值对应 Core Web Vitals 良好标准;资源体积、自定义指标和告警灵敏度仍需根据业务与设备基线确定。
十一、回归发生后的处理流程
紧急线上回归优先止血:关闭 feature flag、回滚版本、延迟第三方或恢复旧资源。完成止血后再补根因修复和防回归测试。
十二、推荐的分层门禁
| 阶段 | 检查 | 失败策略 |
|---|---|---|
| 本地开发 | DevTools/Lighthouse 快速检查 | 提示开发者 |
| PR | bundle diff、关键 URL Lighthouse、多次运行 | 稳定预算 error,噪声项 warn |
| 预发布 | 脚本化核心旅程、真实后端/第三方 | 阻止高风险发布 |
| 灰度 | RUM 按版本和分群对比 | 自动暂停扩量或回滚 |
| 全量 | SLO、趋势、差体验比例 | 告警与性能复盘 |
十三、检查清单
- 性能目标对应真实用户旅程,而不只是首页
- RUM 和实验室数据各自职责明确
- 测试设备、网络、缓存、数据和工具版本可追溯
- 每个关键场景重复运行并保存原始结果
- 同时使用绝对预算和相对回归
- Lighthouse 总分不是唯一 error 门禁
- 自定义指标起点、终点和业务语义一致
- RUM 样本带版本并按设备/路由分群
- 第三方和测试噪声有明确处理策略
- 豁免有负责人、原因和到期时间
- 回归可通过 feature flag 或回滚快速止血
- 优化最终由现场数据和业务指标确认
常见面试问题
Q1: Lighthouse 和 RUM 有什么区别?
答案:
Lighthouse 是受控实验室审计,适合快速诊断和 CI;RUM 来自真实设备、网络、内容和用户行为,适合判断影响面和上线收益。通常用 RUM 发现问题和分群,用实验室复现根因,再回到 RUM 验证。
Q2: 为什么 Lighthouse 每次分数不同?
答案:
CPU 竞争、网络、第三方、测试数据、缓存、浏览器版本都会产生波动。应锁定环境和版本,多次运行取中位结果,保存原始指标,并让稳定的单项预算做 error、总分先做 warning。
Q3: 多次运行应该取平均值还是中位数?
答案:
中位数对偶发异常更稳,适合代表普通一次运行;平均值能反映总体成本但容易被极端值影响。实践中保留全部原始结果,CI 使用中位运行,同时观察最差值和方差。
Q4: 为什么不能只设置绝对阈值?
答案:
页面即使仍在 2.5s 内,也可能从 1.2s 持续退化到 2.4s。绝对阈值保护用户体验底线,相对回归保护已有成果,两者需要同时使用。
Q5: 性能预算超标是否一定阻止合并?
答案:
稳定、可复现且与用户体验直接相关的预算应阻止;噪声高的总分或第三方波动先 warning。确有业务收益的超标可以临时豁免,但要写明负责人、影响、补偿措施和到期时间。
Q6: 如何测试 SPA 的路由切换性能?
答案:
不能只重新加载目标 URL。应从前一页开始执行真实导航,记录点击到新页面关键内容呈现、自定义 route-ready、请求 waterfall、INP 和长任务,同时单独测试 BFCache、预取和软导航测量。
Q7: 为什么 CI 通过,线上仍然慢?
答案:
CI 设备、数据和用户路径有限,常常没有真实第三方、登录态、低端设备和长会话。CI 只能防已建模场景的回归,必须配合 RUM、灰度和生产 SLO。
Q8: 如何选择性能测试设备?
答案:
先看 RUM 设备分布和业务用户,选择覆盖主要流量的中端设备,再补一个高风险低端档位。不要只用开发者高配电脑,也不必穷举所有机型。
Q9: 自定义性能指标有什么风险?
答案:
如果起点终点不一致、只测接口不测呈现、路由间命名不同,就无法比较。指标要有明确用户语义、统一实现、版本和采样策略,并与通用 Web Vitals 互相解释。
Q10: 如何比较两个版本的 RUM?
答案:
让样本带 build id,稳定分桶,在相同路由、设备、网络和地区内比较 P75、差体验比例与业务成功率。确认样本量和时间窗口足够,避免流量结构变化造成辛普森悖论。
Q11: 性能回归如何快速定位到代码变更?
答案:
先确认回归版本和分群,然后二分提交或构建产物,比较 bundle diff、请求瀑布和 Performance trace。将构建 SHA、source map、traceId 与监控样本关联,能显著缩短定位时间。
Q12: 如何让性能治理长期有效?
答案:
把目标落实到关键旅程、负责人、CI 门禁、灰度验证和生产 SLO;性能预算变更要评审,豁免要到期;每次事故补充测试或监控。只有一次专项优化,没有自动化与归属,性能很快会反弹。