跳到主要内容

性能预算

问题

什么是性能预算(Performance Budget)?如何制定和执行性能预算?如何在团队中推行性能文化?

面试速答版

什么是性能预算(Performance Budget)? 性能预算是团队认可的一组上限、目标和退化规则,把“尽量快”变成可验证的工程约束:

  • 资源型预算:按路由、设备和压缩口径定义。例如在关键移动端路由上,可先尝试把首屏必需 JS 的 gzip 预算设为 100~200KB,再根据当前基线与用户数据调整。
  • 体验型预算:线上按页面类型和用户群体看 P75 的 LCP、INP、CLS;LCP 2.5s、INP 200ms、CLS 0.1 是 Core Web Vitals 的“良好”边界,不等于每个 CI 单次运行的稳定门禁值。
  • 回归型预算:同时限制绝对上限和相对退化,例如“LCP 中位数不得退化超过 10%,且不能越过绝对上限”。
  • 规则型预算:关键图片不得懒加载、第三方脚本必须有 owner、图片必须提供尺寸与现代格式候选等。数量规则要说明适用场景,不能把“第三方最多 3 个”当普遍真理。

如何制定和执行性能预算? 三步走:

  • 怎么定:用户任务与业务 SLO + Core Web Vitals 体验边界 + 当前基线 + 竞品参考。竞品受页面功能、地区、缓存和测试条件影响,只能作为输入,不能机械地“快 20%”。
  • 怎么测:构建时检查路由入口、chunk 和增量体积;CI 对固定用户旅程重复运行,使用中位数或稳健统计并同时比较主分支。高噪声实验指标先告警,稳定后再阻断。
  • 怎么报警:线上用 web-vitals 上报真实用户数据(RUM),P75 退化超阈值就钉钉/企微告警。
  • 工具链:本地 webpack-bundle-analyzer 看产物、CI 用 lhci autorun 卡指标、生产用 SpeedCurve / Sentry Performance 持续监控。

如何在团队中推行性能文化?机制和工具而非靠喊口号:

  • 可视化:把性能指标做到看板上(Grafana / 自建大屏),每周看趋势;让性能数据像测试覆盖率一样人人可见。
  • 门禁:把确定性强的产物体积、规则检查设为硬门禁;受环境影响大的实验室指标采用多次运行、基线对比和分级策略,避免误报让团队绕过门禁。
  • 责任制:为每个路由、第三方和预算指定团队 owner;关注系统与模块责任,不把线上退化简单追责到个人。
  • 激励:性能优化纳入 OKR / 季度复盘,分享好的优化案例,让做性能的人有荣誉感。
  • 教育:定期开「性能避坑分享」、Code Review 时主动指出性能反模式(无 key 的列表、行内函数、未拆 chunk 的大依赖)。

答案

性能预算是一组有测量条件、适用范围、owner 和处理流程的约束。它可以是硬上限、目标区间、相对回归阈值或规则检查;不同类型应采用不同门禁强度。


什么是性能预算

定义

性能预算是一组对网页性能指标设定的限制值,用于在开发过程中持续跟踪和控制性能。

为什么需要性能预算

问题没有预算有预算
性能退化难以察觉立即发现
责任划分模糊明确
优化动力不足持续
团队协作各自为政统一标准

预算类型

1. 资源大小预算

以下数字是某个中等复杂度移动端关键路由的示例,不是行业统一阈值。应明确 gzip 还是 Brotli、单文件还是路由总量、首屏必需还是所有异步资源,并对不同路由类型分别配置。

// bundlesize 配置
// package.json
{
"bundlesize": [
{
"path": "./dist/js/*.js",
"maxSize": "200 kB",
"compression": "gzip"
},
{
"path": "./dist/css/*.css",
"maxSize": "50 kB",
"compression": "gzip"
},
{
"path": "./dist/img/**/*.{jpg,png,webp}",
"maxSize": "500 kB"
}
]
}

2. 性能指标预算

指标良好一般
FCP< 1.8s1.8-3s> 3s
LCP< 2.5s2.5-4s> 4s
TBT< 200ms200-600ms> 600ms
CLS< 0.10.1-0.25> 0.25
INP(现场)< 200ms200-500ms> 500ms

LCP、INP、CLS 的分级应在真实用户数据中按页面类型查看第 75 百分位。TBT 是实验室里诊断主线程阻塞的代理指标,不等于 INP;CI 里无法靠一次无交互 Lighthouse 运行直接证明线上 INP 达标。FCP 可作为诊断指标,但不是 Core Web Vitals。

3. 规则型预算

interface RuleBudget {
// 请求数量
maxRequests: number;
maxRequestsPerType: {
script: number;
stylesheet: number;
image: number;
font: number;
};

// 第三方资源
maxThirdPartyRequests: number;
maxThirdPartySize: string;

// 其他规则
requireHTTPS: boolean;
requireCompression: boolean;
requireModernImageFormats: boolean;
}

const budget: RuleBudget = {
maxRequests: 50,
maxRequestsPerType: {
script: 10,
stylesheet: 5,
image: 30,
font: 4,
},
maxThirdPartyRequests: 5,
maxThirdPartySize: '100KB',
requireHTTPS: true,
requireCompression: true,
requireModernImageFormats: true,
};

这段配置同样是示例。在 HTTP/2 或 HTTP/3 下,请求数量的意义与 HTTP/1.1 不同;第三方请求也要按执行成本、隐私、失败隔离和用户价值评估,而不是只数个数。


制定预算策略

基于竞争对手

// 竞品分析脚本
async function analyzeCompetitors(urls: string[]) {
const results = await Promise.all(
urls.map(url => runLighthouse(url))
);

const metrics = results.map(r => ({
url: r.url,
lcp: r.audits['largest-contentful-paint'].numericValue,
fcp: r.audits['first-contentful-paint'].numericValue,
cls: r.audits['cumulative-layout-shift'].numericValue,
}));

// 取最佳值
const bestLCP = Math.min(...metrics.map(m => m.lcp));
const bestFCP = Math.min(...metrics.map(m => m.fcp));
const bestCLS = Math.min(...metrics.map(m => m.cls));

// 示例目标:在相同测试条件下尝试比竞品最佳值快 20%。
// 正式预算还要结合自身功能、当前基线和真实用户数据。
return {
lcpBudget: bestLCP * 0.8,
fcpBudget: bestFCP * 0.8,
clsBudget: Math.min(bestCLS * 0.8, 0.1),
};
}

基于用户体验

// Google Core Web Vitals 推荐值
const coreWebVitalsBudget = {
lcp: 2500, // ms, Good
inp: 200, // ms, Good
cls: 0.1, // score, Good
};

// 更严格的预算(75th percentile)
const strictBudget = {
lcp: 2000,
inp: 100,
cls: 0.05,
};

“更严格”配置只是业务目标示例,不是新的官方分级。字段数据还要标注范围:关键路由、移动端、目标国家/地区、有效样本量和统计窗口。

基于当前基准

// 测量当前性能
async function measureBaseline() {
const runs = 5;
const results: number[] = [];

for (let i = 0; i < runs; i++) {
const result = await runLighthouse(PRODUCTION_URL);
results.push(result.categories.performance.score * 100);
}

const sorted = results.sort((a, b) => a - b);
const baseline = sorted[Math.floor(sorted.length / 2)];

// 中位数只是最小示例;实际还应与同环境主分支结果比较波动区间
return {
performanceScore: Math.floor(baseline),
};
}

预算执行工具

在选工具前,先把预算写成一张可执行矩阵:

字段示例
范围商品详情移动端、搜索结果桌面端、登录后编辑器
用户旅程首次访问、搜索并打开结果、保存表单
条件中端移动设备模拟、冷缓存、固定网络配置;线上为目标地区真实用户
资源口径路由首屏 gzip JS、异步编辑器 chunk、第三方传输与执行成本
体验指标字段 P75 LCP/INP/CLS;实验室 LCP/TBT/长任务
判定绝对上限 + 相对主分支退化 + 最小样本量
Owner业务团队、平台团队、第三方负责人
例外工单、原因、补偿措施、审批人、到期日

一个首页、数据后台和在线编辑器不应共用完全相同的资源预算。可以建立“营销页、内容页、应用页、重交互页”等模板,再允许路由在有理由时覆盖。

Lighthouse Budget

// lighthouse-budget.json
{
"timings": [
{
"metric": "first-contentful-paint",
"budget": 1800
},
{
"metric": "largest-contentful-paint",
"budget": 2500
},
{
"metric": "cumulative-layout-shift",
"budget": 0.1
}
],
"resourceSizes": [
{
"resourceType": "script",
"budget": 200
},
{
"resourceType": "stylesheet",
"budget": 50
},
{
"resourceType": "image",
"budget": 500
},
{
"resourceType": "total",
"budget": 1000
}
],
"resourceCounts": [
{
"resourceType": "script",
"budget": 10
},
{
"resourceType": "third-party",
"budget": 5
}
]
}
# 使用预算运行 Lighthouse
lighthouse https://example.com --budget-path=./lighthouse-budget.json

Webpack Performance

// webpack.config.ts
import type { Configuration } from 'webpack';

const config: Configuration = {
performance: {
hints: 'error', // 'warning' | 'error' | false
maxAssetSize: 250 * 1024, // 250KB
maxEntrypointSize: 400 * 1024, // 400KB
assetFilter: (assetFilename) => {
return !/\.map$/.test(assetFilename);
},
},
};

export default config;

Bundle Analyzer

// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default {
plugins: [
visualizer({
filename: 'stats.html',
gzipSize: true,
brotliSize: true,
}),
],
};

bundlesize

// package.json
{
"scripts": {
"check-size": "bundlesize"
},
"bundlesize": [
{
"path": "dist/js/main.*.js",
"maxSize": "150 kB",
"compression": "gzip"
},
{
"path": "dist/js/vendor.*.js",
"maxSize": "100 kB",
"compression": "gzip"
}
]
}

CI/CD 集成

GitHub Actions

下面展示集成位置,Action 与 Node 版本应按仓库支持矩阵固定并定期升级,不能把示例版本当成长期最新值。

# .github/workflows/performance.yml
name: Performance Budget Check

on:
pull_request:
branches: [main]

jobs:
budget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

# 检查 bundle 大小
- name: Check bundle size
run: npm run check-size

# Lighthouse 预算检查
- name: Lighthouse Budget
uses: treosh/lighthouse-ci-action@v11
with:
configPath: ./lighthouserc.json
budgetPath: ./lighthouse-budget.json

# 上传结果到 PR
- name: Comment on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
path: ./lighthouse-report.md

自定义预算检查

// scripts/check-budget.ts
import fs from 'fs';
import path from 'path';
import zlib from 'zlib';

interface BudgetConfig {
path: string;
maxSize: number; // bytes
compression?: 'gzip' | 'brotli' | 'none';
}

const budgets: BudgetConfig[] = [
{ path: 'dist/js/main.*.js', maxSize: 150 * 1024, compression: 'gzip' },
{ path: 'dist/css/*.css', maxSize: 30 * 1024, compression: 'gzip' },
];

async function checkBudgets() {
const violations: string[] = [];

for (const budget of budgets) {
const files = globSync(budget.path);

for (const file of files) {
const content = fs.readFileSync(file);
let size = content.length;

if (budget.compression === 'gzip') {
size = zlib.gzipSync(content).length;
} else if (budget.compression === 'brotli') {
size = zlib.brotliCompressSync(content).length;
}

if (size > budget.maxSize) {
violations.push(
`${file}: ${formatSize(size)} > ${formatSize(budget.maxSize)}`
);
}
}
}

if (violations.length > 0) {
console.error('Budget violations:');
violations.forEach(v => console.error(`${v}`));
process.exit(1);
}

console.log('✅ All budgets passed');
}

function formatSize(bytes: number): string {
return `${(bytes / 1024).toFixed(2)} KB`;
}

checkBudgets();

监控和告警

实时监控

import { onCLS, onINP, onLCP, type Metric } from 'web-vitals/attribution';

function reportWebVital(metric: Metric): void {
const payload = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
delta: metric.delta,
rating: metric.rating,
navigationType: metric.navigationType,
route: location.pathname,
});

navigator.sendBeacon('/api/performance/vitals', payload);
}

onLCP(reportWebVital);
onINP(reportWebVital);
onCLS(reportWebVital);

生产侧不要按单个用户样本即时报警。应按页面类型、设备、地区和版本聚合,检查样本量与发布标记,再比较滚动窗口 P75、历史基线和变化幅度。URL、DOM 选择器和 attribution 字段需要脱敏与限基数。

告警配置

// 告警规则配置
interface AlertRule {
metric: string;
threshold: number;
operator: '>' | '<' | '==' | '>=' | '<=';
duration: string; // e.g., '5m'
severity: 'warning' | 'error' | 'critical';
channels: ('email' | 'slack' | 'pagerduty')[];
}

const alertRules: AlertRule[] = [
{
metric: 'p75_lcp',
threshold: 2500,
operator: '>',
duration: '10m',
severity: 'warning',
channels: ['slack'],
},
{
metric: 'p95_lcp',
threshold: 4000,
operator: '>',
duration: '5m',
severity: 'critical',
channels: ['slack', 'pagerduty'],
},
];

团队协作

预算文档

# 性能预算文档

## 当前预算

| 指标 | 预算值 | 当前值 | 状态 |
|------|--------|--------|------|
| LCP | 2.5s | 2.1s ||
| FCP | 1.8s | 1.5s ||
| CLS | 0.1 | 0.08 ||
| JS Size | 200KB | 180KB ||
| Total Size | 1MB | 850KB ||

## 预算变更历史

| 日期 | 指标 | 旧值 | 新值 | 原因 |
|------|------|------|------|------|
| 2024-01-15 | LCP | 3.0s | 2.5s | Core Web Vitals 更新 |

## 例外情况

需要超出预算时,必须:
1. 提交例外申请
2. 获得技术负责人批准
3. 记录原因、影响范围、补偿措施和预计恢复时间
4. 设置到期日,由自动化在到期前提醒并在到期后重新阻断

PR 检查清单

## 性能检查清单

- [ ] 新增的 JS/CSS 是否影响首屏?
- [ ] 图片是否使用现代格式(WebP/AVIF)?
- [ ] 是否添加了不必要的依赖?
- [ ] bundle size 是否在预算内?
- [ ] Lighthouse 分数是否达标?
- [ ] 关键用户旅程相对主分支是否退化?
- [ ] 新增第三方是否有 owner、超时、降级和隐私评估?

常见面试问题

Q1: 什么是性能预算?为什么需要它?

答案

性能预算是设定的性能指标阈值,超出即视为问题。

必要性

  1. 防止退化:每次变更都检查
  2. 明确目标:团队共同遵守
  3. 量化性能:从主观变客观
  4. 持续关注:性能是持续过程

Q2: 如何制定合理的性能预算?

答案

策略方法适用场景
竞品分析统一地区、设备、缓存和页面功能后作为参考新项目
基准测量不允许退化成熟产品
用户期望Core Web Vitals通用

我通常会先按路由和核心用户旅程分组,写清设备、网络、缓存、地区和统计口径。预算同时包含绝对上限与相对主分支阈值:前者守住体验底线,后者尽早发现小幅回归。对于现状较差的页面,可以设置分阶段目标,但要有时间表和 owner。

Q3: 如何在 CI/CD 中执行预算检查?

答案

CI 中应分层处理:产物体积、重复依赖、关键资源规则比较确定,可以直接阻断;Lighthouse 等实验室指标有噪声,应固定环境、预热并重复运行,用中位数和主分支对比,必要时先告警后阻断。

# GitHub Actions
steps:
- name: Build
run: npm run build

# 1. Bundle size 检查
- name: Check bundle size
run: npx bundlesize

# 2. Lighthouse 检查
- name: Lighthouse
uses: treosh/lighthouse-ci-action@v11
with:
budgetPath: ./budget.json

# 3. 自定义检查
- name: Custom budget check
run: node scripts/check-budget.js

还要跑代表性的多页面、多步骤旅程,而不只是首页一次加载。失败报告应展示本次值、基线值、绝对预算、波动范围和产物差异,方便开发者判断是代码回归还是测试噪声。

Q4: 预算超标如何处理?

答案

  1. 阻止合并:CI 失败
  2. 分析原因:是否必要
  3. 优化或申请例外
  4. 记录变更并设置例外到期日
// 例外流程
interface BudgetException {
ticket: string; // 关联 issue
approver: string; // 审批人
deadline: Date; // 恢复期限
currentValue: number; // 当前值
budgetValue: number; // 预算值
reason: string; // 原因
}

Q5: 如何让团队重视性能?

答案

  1. 可视化:Dashboard 展示性能趋势
  2. 自动化:CI 强制检查
  3. 责任制:路由、模块和第三方都有明确 owner
  4. 教育:定期分享性能知识
  5. 激励:性能优化的认可

关键是让数据可解释、门禁可信、修复路径清楚。如果噪声测试经常误报,团队很快会绕过它;如果只有“谁写坏了”的追责,也会抑制协作。更好的机制是把预算纳入需求评审、PR 报告、发布观测和复盘,并给平台团队建设公共工具的责任。

相关链接