Lighthouse 与 Performance 性能证据采集工具手册
CI 掉了十二分,本地却复现不出来
性能回归经常从一张红色 CI 截图开始:同一页面前一次 92 分,这一次 80 分;开发机连续运行又回到 90 分以上。若团队立刻调整阈值,门禁会失去价值;若把单次分数当成代码事实,又会追错方向。Lighthouse 分数是特定 Chrome、Lighthouse、CPU、网络、缓存、页面状态和样本内容共同作用的结果,真正可复核的证据必须保留原始指标、运行环境和 trace。
先把工具角色分开。Lighthouse 自动加载页面并执行一组审计,适合发现页面加载阶段的回退和通用质量问题;Chrome DevTools Performance 记录主线程、网络、渲染、帧和用户交互,适合解释“时间花在哪里”;Web Vitals 描述用户体验指标;Lighthouse CI 负责重复采集、断言和报告流转。它们不能互相替代,也不能替代真实用户监控。
Web Vitals 官方说明把当前 Core Web Vitals 定为 LCP、INP 和 CLS。良好体验参考值分别是 LCP 不高于 2.5 秒、INP 不高于 200 毫秒、CLS 不高于 0.1,并建议按移动端和桌面端分别观察第 75 百分位。Lighthouse 没有真实用户输入,不能测量 INP;它使用实验室可测的 TBT 帮助发现潜在交互阻塞。把 TBT 写成 INP,或把一次本地 LCP 当成生产第 75 百分位,都会制造错误结论。
先固定一条实验跑道
第一次采集选择稳定的本地构建或测试环境,不要直接拿持续变化的生产页面建立 CI 基线。记录这些事实:页面 URL、构建提交、Chrome 和 Lighthouse 版本、mobile 或 desktop、throttling 方法、缓存状态、登录状态、测试数据、runner 规格与运行次数。扩展、杀毒软件、A/B 实验、广告、远程字体和第三方脚本都可能增加噪声。
DevTools 入口已经随桌面 Chrome 提供:
Chrome DevTools -> Lighthouse
Chrome DevTools -> Performance命令行和 Node 模块需要本机安装 Chrome。当前稳定的 Lighthouse 13.4.0 要求 Node.js >=22.19;Lighthouse CI 当前稳定版为 0.15.1。项目应把实际使用的版本锁进 lockfile,不让 CI 每次解析最新包:
npm install --save-dev --save-exact lighthouse@13.4.0 @lhci/cli@0.15.1
npx lighthouse --version
npx lhci --version这两个版本号不能证明两条命令使用同一 Lighthouse 内核:@lhci/cli@0.15.1 自身依赖 Lighthouse 12.6.1,直接运行的 npx lighthouse 则是 13.4.0。比较报告时应读取 JSON 中的 lighthouseVersion,不能只记录 LHCI 版本。若项目运行时低于 Node 22.19,可把直接 Lighthouse 任务放进单独的工具环境;Chrome 版本、系统字体和共享 runner CPU 仍需固定,否则只固定了 Node 包。
节流方法决定证据能否比较
simulate 是 Lighthouse CLI 和 DevTools Lighthouse 面板的默认方法。它先观察一次页面加载,再用 Lantern 模型推演较慢网络与 CPU 下的指标,运行较快、波动通常较小,适合固定 runner 上的持续回归;代价是原始 trace 发生在模拟之前,trace 时间不会与报告中的模拟指标逐点相等。
devtools 会在采集时通过 Chrome DevTools 对请求和 CPU 实际施加节流,报告与 View Trace 中的时间能够对齐,适合解释单次调用链;它更慢,也更容易受宿主机竞争和请求级网络模型限制。provided 表示 Lighthouse 不再施加自己的节流,只接受外部环境已经提供的条件,只有在真实设备、受控网络整形或另一层测试设施已经固定资源时才有意义,不能把开发机上的 provided 结果与默认移动基线直接比较。
团队必须把 preset、throttlingMethod、Chrome、runner 和缓存状态作为同一个基线整体版本化。更换节流方法等于更换实验,不是普通参数微调;旧阈值不能直接沿用。
用正反实验认识分数和原始指标
准备一个生产构建页面,在独立 Chrome profile 或 CI runner 中运行三次以上。单次本地快速检查可以这样启动:
npx lighthouse http://localhost:3000/ \
--preset=desktop \
--throttling-method=simulate \
--only-categories=performance \
--output=json \
--output=html \
--output-path=./artifacts/lighthouse多输出模式会在路径后附加标准扩展名,上述命令应生成 lighthouse.report.html 与 lighthouse.report.json;流水线仍应检查两个文件是否存在且 JSON 可解析。HTML 用于人工定位,JSON 保留 lighthouseVersion、fetchTime、finalDisplayedUrl、环境、分类分数、audit 的 numericValue 和明细,适合机器比较。命令退出成功只表示审计完成,不表示性能达标。
正向实验使用正常生产构建,连续运行并保存每次 LCP、CLS、TBT、FCP、Speed Index 和总分。预期结果不是“必须 90 分”,而是大多数样本聚集在可解释的区间,且没有页面加载错误。若同一提交的离散程度已经大于团队准备阻断的回归幅度,当前 runner 不具备设置该阈值的条件。
反向实验在测试分支给首屏加入一个明确阻塞:
const startedAt = performance.now();
while (performance.now() - startedAt < 600) {
// Deliberately block the main thread in a disposable experiment branch.
}重建后用相同环境和次数运行。预期在 main thread、long task 与 TBT 上出现明显恶化,Performance trace 能定位到对应脚本调用。实验完成后删除阻塞代码并重建,确认指标回到原有分布。若没有明显变化,先检查审计是否命中了新构建、Service Worker 是否仍提供旧资源、页面是否在阻塞代码执行前结束采集,而不是先扩大阻塞时间。
这个反例用于验证测量链对主线程回归是否敏感,不能证明生产用户的 INP 已经改善或恶化。真实 INP 需要现场数据或包含真实交互的受控测量。
Performance trace 把症状还原成调用链
Lighthouse 告诉你“哪个指标异常”,Performance 面板负责解释“哪段工作导致异常”。加载问题使用 Record and reload;搜索、展开菜单、切换路由等运行时问题使用 Record,执行一次短交互后立即停止。长录制会把无关定时器、后台请求和多次交互混在一起,使因果链不可读。
分析时按事件链推进:
在 Timings 和 Web Vitals 标记中定位 LCP、CLS 或交互窗口。在 Network 轨道确认关键资源的排队、连接、TTFB 与下载阶段。在 Main 火焰图寻找 long task,展开到具体 Function Call。
用 Bottom-up 按 Self Time 找直接耗时函数,用 Call tree 还原调用来源。对布局问题检查 Layout、Recalculate Style、Layout Shift 及其影响节点。对自定义业务阶段加入 performance.mark() 和 performance.measure(),让 trace 能对齐业务动作。
例如搜索按钮点击后 700 毫秒才更新,trace 若显示请求很快返回、随后出现长 JavaScript task,瓶颈在客户端计算或渲染;若主线程空闲而 Network 长时间等待响应,owner 更可能在后端、网关或网络。只看黄色脚本块或总下载量,无法完成责任定位。
Performance 面板也可以显示现场数据,但现场与实验室数据的来源和聚合口径必须分开。现场第 75 百分位说明一群真实访问的分布,单条 trace 解释一次访问的调用链;正确工作流是用现场数据发现页面族和用户段,再用可复现交互录制 trace。
Lighthouse CI 阈值必须绑定分布
Lighthouse CI 的 autorun 会串联 collect、assert 和 upload。它默认对每个 URL 采集多次,配置中的 numberOfRuns 可以提高样本数;断言支持 median、optimistic、pessimistic 和 median-run 等聚合方式。使用哪一种必须写进配置,不能让评审者猜测一条失败来自最差样本还是中位样本。
一个项目级配置可以从可解释的小集合开始:
module.exports = {
ci: {
collect: {
url: ["http://localhost:4173/", "http://localhost:4173/search"],
startServerCommand: "npm run preview -- --port 4173",
startServerReadyPattern: "localhost:4173",
numberOfRuns: 5,
settings: {
preset: "desktop",
throttlingMethod: "simulate",
},
},
assert: {
assertions: {
"categories:performance": ["warn", {
minScore: 0.85,
aggregationMethod: "median-run",
}],
"largest-contentful-paint": ["error", {
maxNumericValue: 2500,
aggregationMethod: "median",
}],
"cumulative-layout-shift": ["error", {
maxNumericValue: 0.1,
aggregationMethod: "pessimistic",
}],
"total-blocking-time": ["error", {
maxNumericValue: 300,
aggregationMethod: "median",
}],
},
},
upload: {
target: "filesystem",
outputDir: "./artifacts/lhci",
},
},
};这些数字是演示配置,不能直接复制成生产 SLO。LCP 和 CLS 示例接近 Core Web Vitals 的良好参考值,但实验室负载不是现场第 75 百分位;TBT 也不是 INP。真实门禁应先在固定 runner 上采集一段基线,估计自然波动,再根据页面类型、业务 SLO 和可容忍回退设定 warn 与 error。
总分适合趋势提示,原始指标和资源预算更适合阻断。总分权重和审计内容会随 Lighthouse 版本演进,同一页面升级工具后可能无代码变化也改分。升级 PR 应保存旧版和新版报告,先解释权重、audit 与环境差异,再迁移阈值。
运行前先做健康检查,再执行门禁:
npx lhci healthcheck --fatal
npx lhci autorun预期 collect 为每个 URL 生成多份报告,assert 对配置项给出 pass、warn 或 failure,error 级失败返回非零退出码。页面启动失败、Chrome 启动失败和指标越界是三类不同证据,CI 日志必须保留失败阶段,不能统一显示成“性能不达标”。
登录页、测试数据与浏览器状态
登录页面需要让 LHCI 使用的 Chrome 获得测试会话。可以通过受控的 Puppeteer 脚本登录或注入状态,但凭证只从 CI secret 读取。测试账号使用最小权限和假数据;脚本日志不得打印密码、Cookie、Authorization header 或完整跳转 URL。
缓存策略也会改变结果。Lighthouse 默认会重置部分浏览器存储;disableStorageReset 会保留 localStorage、IndexedDB 等状态,适合专门的回访实验,却可能让不同运行互相污染。冷启动与暖缓存是两个实验场景,应分别命名 URL 或 job,不要在同一基线中混合。
Service Worker 是常见隐藏变量。本地改了资源但报告仍引用旧 hash 时,在 Application 面板确认活动 worker、Cache Storage 和客户端控制状态;CI 采用全新 profile 通常更稳定。修复验证需要重新构建、重启预览服务并确认 Network 的资源 hash 已变化。
噪声不是靠重试掩盖的
同一提交波动很大
查看每次报告的 Chrome、Lighthouse、runner、页面最终 URL 和原始指标。若多个指标一起抖动,优先怀疑 CPU 争用、网络、第三方内容或测试数据;若只有 CLS 抖动,检查轮播、字体、广告位和异步内容尺寸。增加运行次数可以估计分布,但“失败就重跑直到通过”会选择性丢弃坏样本。
本地高分,生产现场仍差
按设备、网络、地域、页面模板和登录状态查看现场第 75 百分位。实验室可能没有 CDN miss、慢后端、低端设备、长会话和真实交互。用现场异常分组选择复现场景,再让 Performance trace 解释具体调用链;不能调低现场目标来匹配开发机。
CI 报 NO_FCP 或页面加载错误
先查看最终 URL、HTTP 状态、控制台错误、证书、DNS、代理和启动服务日志。登录重定向、空白错误页和被 CSP 拦截的脚本都会让指标缺失。页面未成功加载时没有性能结论,修复连通性后重新采集。
TBT 下降,INP 没改善
TBT 反映加载期间主线程阻塞,INP 覆盖真实页面生命周期中的交互。检查用户触发的事件处理、异步任务、渲染更新和长会话状态,录制目标交互 trace,并用 RUM 分解 INP。二者相关但不等价。
trace 太大、导入缓慢
缩短录制窗口,只保留一次加载或一次交互。关闭不需要的截图、资源内容和 source map 后再导出;不能靠删除随机 trace 事件破坏证据完整性。Chrome 支持 gzip 压缩 trace,可降低存储和传输成本。
Trace 和报告属于敏感制品
Chrome 的 trace 保存说明允许把注释、资源内容和 source map 一并保存。资源内容会包含 HTML、JavaScript 与 CSS;source map 可能暴露源文件名、目录结构和未压缩代码;截图、URL、query、User Timing 名称和业务文本也可能含用户数据。启用这些选项能改善诊断,但同时提高密级。
分享前执行以下处理:使用测试账号和假数据;移除 URL 中的敏感 query;关闭不必要的资源内容和 source map;检查截图与注释;限制 artifact 访问和保留期。不要把 trace 当普通 JSON 传到公共 issue、临时公共存储或在线 viewer。公共 Viewer 是否上传、处理或保留数据应在使用前确认;内部页面优先在本地 DevTools 打开。
LHCI 的上传目标同样是数据边界。temporary-public-storage 产生可访问报告链接,不适合登录态或内网页面。文件系统 artifact、受控对象存储或自托管 LHCI server 都需要访问控制、加密、审计、容量上限和删除策略。GitHub App token、个人 token、LHCI build token 与 admin token应按权限分级,放入 CI secret 并在人员或平台变更时轮换。
容量、成本与长期维护
性能门禁的成本由 URL 数量、每个 URL 的运行次数、Chrome 启动时间、runner 规格、报告体积和保留周期共同决定。五个 URL 各跑五次就是二十五次页面加载;若每次还保存 HTML、JSON、trace 和截图,制品会快速膨胀。记录 job 时长、重试率、报告总量、存储增速和每个页面的告警命中率,删除长期没有决策价值的审计对象。
页面分层比所有 URL 使用同一阈值更有效:首页关注 LCP 与资源预算,交互型后台关注 TBT、长任务和关键交互 trace,内容页关注 CLS 与图片尺寸。每类选择少量代表路由进入每次 PR;全量页面放到定时任务,现场 RUM 持续观察真实分布。这样既控制 runner 成本,也避免门禁被噪声淹没。
版本升级采用可回滚的双跑:锁住旧版 Lighthouse、LHCI 和 Chrome,再让新工具对同一构建、同一 runner、同一 URL 集运行;比较原始指标、audit 集合、总分和报告结构。确认差异后更新 lockfile、runner 镜像与阈值基线。出现异常时恢复依赖锁、Chrome 镜像和配置,不需要回滚业务代码。
性能例外必须有 owner、原因、到期条件和证据链接。一个指标降级若经过架构取舍被接受,应记录新增资源或主线程成本如何被业务收益抵消,以及何时重新评估。永久关闭 audit、不断放宽阈值或无限重试,都会把 CI 变成昂贵的绿色装饰。
最终需要守住几个不变量:同一基线使用同一版本与环境;单次总分不触发架构结论;实验室指标不冒充现场分位数;Lighthouse 的 TBT 不冒充 INP;每个故障能回到资源、任务、布局或调用栈;报告和 trace 有权限、保留与删除策略。这样采集到的才是可审查、可回归、可治理的性能证据。
