配了三年 Lighthouse 分数挺好看,上线用户说卡——今天 Performance Budget 把这件事从制度上彻底变了

配了三年性能优化,每次版本发布前都盯着 Lighthouse 跑分——分数一出就上线,上线之后用户说卡,再回来突击优化一周,然后继续劣化。这就是很多前端团队正在经历的”波浪式”性能曲线,而 Lighthouse CI 根本管不住它。

问题出在 Lighthouse 是实验室数据,不是真实用户体验。

Lighthouse 在自己的机器上、用固定网络模拟跑分——跑过了不代表你的用户那天用的中端 Android 机、地铁里信号不好的时候也能过得去。Core Web Vitals 的阈值本身就是按真实用户第 75 百分位设定的,结果你拿合成测试来对抗真实用户数据,两个根本不在一个维度上。

Performance Budget 就是把这三个维度拧到一起去,让性能从”靠自觉”变成”靠制度”。

Performance Budget 分三层

第一层是 Field 预算,从真实用户数据里取 Core Web Vitals 的 p75 值。工具是 web-vitals 或者 Datadog/Sentry,是你真正的性能基准线。Field 预算告诉团队:用户实际感受到的是什么。

第二层是 Lab 预算,跑在 CI 里,用 Lighthouse CI 对比分支差异。你给 FCP 设个上限比如 1.8 秒,Lighthouse CI 就会在 PR 里告诉你这次提交导致 FCP 增加了多少毫秒,超过了直接 block 合并。Steve Kinney 在他的性能课程里建议用 TBT 作为 INP 的 CI 替代指标,因为 Lighthouse 本身测不了 INP,但 TBT 能反映主线程的阻塞程度。

第三层是 Build-time 预算,直接在构建时卡住,不需要等到 CI 运行。webpack 的 performance.maxAssetSize 和 performance.maxEntrypointSize 会让你的构建在 bundle 超大时直接失败;size-limit 可以对主 bundle、vendor chunk、CSS 分别设 gzip 后的 KB 数上限。

三层之间的关系是:Field 告诉你用户真正经历了什么,Lab 在你提交代码时验证,Build-time 在打包时直接拦截。Budget 这件事不是在 Dashboard 里好看而已,它必须成为发布流程的一部分才有意义。

assertMatrix:不同路由、不同预算

一个产品里落地页、后台页面、结账页面的性能目标显然不一样,放在一起用同一个 Lighthouse 断言必然会遇到矛盾——要么松到没有意义,要么紧到天天 block 团队。

Lighthouse CI 的 assertMatrix 就是来解决这个的:

{
  "assertions": {
    "resource-summary:script:size": ["error", { "maxNumericValue": 200000 }]
  },
  "assertMatrix": [
    {
      "path": "/",
      "assertions": {
        "first-contentful-paint": ["error", { "maxNumericValue": 1800 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "total-blocking-time": ["error", { "maxNumericValue": 300 }]
      }
    },
    {
      "path": "/dashboard",
      "assertions": {
        "first-contentful-paint": ["error", { "maxNumericValue": 3000 }],
        "total-blocking-time": ["error", { "maxNumericValue": 500 }]
      }
    }
  ]
}

落地页要求更紧,后台页面允许更宽松,CI 里按路由分别断言,不再一刀切。

这套东西在 CI 里长什么样

GitHub Actions 里跑 Lighthouse CI,PR 的 comment 底下会直接出现这样的输出:

✅ Performance: 94 (budget: 90)
✅ LCP: 2.1s (budget: 2.5s)
✅ TBT: 140ms (budget: 200ms)
✅ CLS: 0.05 (budget: 0.1)
✅ JS size: 187KB (budget: 250KB)
❌ Total page weight: 623KB (budget: 600KB)

失败的那行告诉你具体超了多少 KB,开发者直接去 PR diff 里找原因,不需要再靠人肉猜瓶颈在哪里。

还有一种场景比单个 PR 增量更隐蔽:每个 PR 加一个脚本、一小块 CSS,看起来都没超,但三个月后 bundle 从 300KB 涨到了 800KB。Build-time 的 size-limit 就是用来卡这种慢性退化的——每次构建都会检查,主 bundle 超过 250KB 直接构建失败,不需要等到 Lighthouse 来发现。

一个具体的起步预算

不知道怎么设数字?从 Web Almanac 的中位数开始,然后逐步收紧。Steve Kinney 建议的起步形状是:生产环境里 p75 LCP ≤ 2.5s、INP ≤ 200ms、CLS ≤ 0.1、TTFB ≤ 0.8s,移动端和桌面端分开设。CI 里的 FCP ≤ 1.8s、TBT < 200ms,然后加上路由级别的 JS 体积上限。Build-time 里主 chunk gzip 后 < 250KB,总资源 gzip 后 < 600KB,第三方请求数 < 10。

warning 设在 hard ceiling 的 85%~90%,error 设在 ceiling 本身。团队先适应 warning,一段时间后把稳定下来的 warning 升格为 error,这样不会一开始就让团队觉得预算太严而不执行。

下一步该做什么

先把 size-limit 配进 package.json 和 CI 里,这个最简单也最直观,配完跑一次看现状是多少,那就是你的第一个基准线。然后配 Lighthouse CI 的 assert,把你核心页面的 FCP/LCP/CLS 目标写进去,用 assertMatrix 按路由区分。再接入 CrUX 或者 web-vitals RUM,让 Field 数据真正跑起来。最后把 Field 目标和 CI 目标对齐,确保两边用同一套数字说话。

性能这件事,从”大家注意一下”到”不过 CI 就是不让过”,中间差的不是工具,是制度。


三个证据来源:

  1. Steve Kinney 的 Performance Budgets 课程(GitHub 公开资料),提出三层预算体系:Field 预算捕获真实用户体验、Lab 预算在 CI 里对比分支差异、Build-time 预算在打包时直接拦截,附具体起步阈值建议
  2. the-practical-developer.online 的 Lighthouse CI 实操文章,展示了 assertMatrix 按路由分预算的真实配置和 PR 里的输出格式,以及每周定期跑生产审计的趋势追踪方案
  3. OpenReplay 的 2026 性能优化决议,从 Field vs Lab 数据差距切入,说明为什么 INP 的根因是累积主线程压力而非单个交互,解释 Field 数据作为性能基准线的必要性

评论区

0 条评论

登录后可评论。

阿速·性能优化 11 阅读