TL;DR: 性能预算是应用必须遵守的一组约定阈值——涵盖资源包大小、加载时间、图片重量和 API 延迟。没有预算,性能会随着每个功能的加入而逐步退化。本文介绍如何在 CI/CD 中定义、测量和执行性能预算。
为什么性能预算很重要
性能衰减静默发生。每个功能都会增加 JavaScript、图片和 API 调用。单独来看,每次增加都不易察觉。但累积下来,6 到 12 个月后,一个初始加载 1.5 秒的页面会变成一个需要 4 秒加载的页面。
| 功能 | 新增 JS | 新增延迟 | 累积(6 个月) |
|---|---|---|---|
| 数据分析仪表板 | 18 KB | +100ms | +100ms |
| 团队协作功能 | 25 KB | +200ms | +300ms |
| 文件上传界面 | 12 KB | +80ms | +380ms |
| 数据导出 | 8 KB | +50ms | +430ms |
| 聊天集成 | 35 KB | +300ms | +730ms |
没有预算,730ms 的累积增量会一直被忽视,直到用户开始抱怨——或者你的 Core Web Vitals 分数下降,SEO 排名随之滑落。
定义预算
指标分类
// performance-budget.config.ts
export const performanceBudget = {
// 数量预算 —— 在构建时追踪
javascript: {
totalInitial: 150 * 1024, // 150 KB (gzip 后)
routeLevel: 50 * 1024, // 每个路由分片 50 KB
},
css: {
totalInitial: 30 * 1024, // 30 KB
criticalPath: 15 * 1024, // 关键路径内联 15 KB
},
images: {
heroImage: 200 * 1024, // 200 KB
thumbnail: 50 * 1024, // 50 KB
totalPageWeight: 500 * 1024, // 总页面重量 500 KB
maxImageCount: 15, // 每页最多图片数
},
fonts: {
total: 40 * 1024, // 40 KB
maxCustomFonts: 2, // 最多字体家族数
},
// 时序预算 —— 在 CI 和 RUM 中追踪
timing: {
lcp: 2500, // < 2.5s
cls: 0.1, // < 0.1
inp: 200, // < 200ms
ttfb: 800, // < 800ms
firstContentfulPaint: 1800, // < 1.8s
timeToInteractive: 3500, // < 3.5s
},
// 网络预算
network: {
totalRequests: 50, // 最多 HTTP 请求数
thirdPartyScripts: 5, // 最多第三方脚本数
blockingScripts: 0, // 无渲染阻塞脚本
},
}
使用 Lighthouse CI 执行 CI/CD 自动化
将性能预算集成到 CI 流水线中,可以确保任何未达标的 PR 都无法合并。以下 GitHub Actions 工作流在每次 Pull Request 时运行 Lighthouse CI 和资源包大小检查:
# .github/workflows/performance.yml
name: Performance Budget
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run build
- run: npm run start:preview &
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun --config=lighthouserc.json
- name: Check Bundle Size
run: |
npm run build -- --report
node scripts/check-bundle-size.mjs ./dist/report.html
- name: Check Image Budget
run: |
node scripts/check-image-budget.mjs ./dist/public
- name: Comment PR on Failure
if: failure()
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Performance budget check failed. See details in the workflow run.'
})
Lighthouse CI 配置
Lighthouse CI 断言定义了每个性能指标通过或失败的标准。此配置强制执行 LCP、CLS、INP 等 Core Web Vitals 阈值,不同指标使用不同严重级别:
{
"ci": {
"collect": {
"numberOfRuns": 3,
"staticDistDir": "./dist/public",
"settings": {
"throttlingMethod": "devtools",
"formFactor": "desktop"
}
},
"assert": {
"assertions": {
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"interaction-to-next-paint": ["error", { "maxNumericValue": 200 }],
"total-blocking-time": ["error", { "maxNumericValue": 200 }],
"max-potential-fid": ["warn", { "maxNumericValue": 100 }],
"uses-responsive-images": "error",
"offscreen-images": "error",
"unminified-javascript": "error",
"unminified-css": "error",
"render-blocking-resources": ["warn", { "maxLength": 3 }]
}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
资源包大小执行
JavaScript 资源包大小是可控的性能杠杆中最具影响力的之一。以下脚本将构建产物与预设大小预算进行比对,任一资源包超出限制时将导致 CI 构建失败:
// scripts/check-bundle-size.mjs
import { readFile } from "fs/promises"
import path from "path"
const BUDGET = {
"main": 150 * 1024, // 主入口:150KB
"vendor": 50 * 1024, // 供应商分片:50KB
}
async function checkBundleSize() {
const source = path.join(process.cwd(), "dist", "report.html")
const content = await readFile(source, "utf-8")
const failures = []
for (const [key, max] of Object.entries(BUDGET)) {
const size = extractBundleSize(content, key)
if (size && size > max) {
failures.push(`${key}: ${formatSize(size)} > ${formatSize(max)}`)
}
}
if (failures.length > 0) {
console.error("Bundle size budget failed:")
failures.forEach((f) => console.error(` ${f}`))
process.exit(1)
}
console.log("Bundle size budget passed")
}
预算超限时的自动化告警
除 CI 执行外,对生产环境性能数据进行实时监控,有助于发现那些仅在真实用户条件下才会暴露的性能退化。以下服务端函数查询 RUM 数据和构建产物,生成性能告警:
// server/monitoring/performance-alerts.ts
export const checkPerformanceAlert = createServerFn({ method: "GET" }).handler(
async ({}, { context }) => {
const results = []
// 从 RUM 数据检查时序预算
const vitals = await context.env.DB.prepare(`
SELECT name, AVG(value) as avg, PERCENTILE(value, 95) as p95
FROM web_vitals
WHERE created_at > datetime('now', '-1 hour')
GROUP BY name
`).all()
const timingBudget = {
LCP: 2500,
CLS: 0.1,
INP: 200,
TTFB: 800,
}
for (const row of vitals.results) {
const threshold = timingBudget[row.name as keyof typeof timingBudget]
if (threshold && Number(row.p95) > threshold) {
results.push({
type: "performance_regression",
metric: row.name,
p95: row.p95,
threshold,
severity: "warning",
})
}
}
// 从构建产物检查资源包大小
const bundleSize = await context.env.DB.prepare(`
SELECT route, SUM(size) as total
FROM bundle_sizes
WHERE version = (SELECT MAX(version) FROM bundle_sizes)
GROUP BY route
`).all()
const bundleBudget = 150 * 1024 // 150KB
const oversized = bundleSize.results.filter(
(r) => Number(r.total) > bundleBudget
)
if (oversized.length > 0) {
results.push({
type: "bundle_size_exceeded",
routes: oversized.map((r) => `${r.route}: ${formatSize(r.total)}`),
severity: "warning",
})
}
return results
}
)
预算可视化仪表板
实时仪表板让团队随时掌握当前性能状态。以下 React 组件以颜色编码状态指示器展示每个指标与其预算阈值的对比:
// Dashboard component showing budget status
function PerformanceBudgetDashboard() {
const { data: budgets } = useQuery({
queryKey: ["performance-budget"],
queryFn: () => checkPerformanceAlert(),
refetchInterval: 60_000, // 每分钟刷新
})
return (
<div className="grid grid-cols-3 gap-4">
{Object.entries(timingBudget).map(([metric, budget]) => (
<BudgetCard
key={metric}
metric={metric}
current={currentValues[metric]}
budget={budget}
/>
))}
</div>
)
}
function BudgetCard({ metric, current, budget }: BudgetCardProps) {
const ratio = current / budget
const status = ratio > 1 ? "failing" : ratio > 0.8 ? "warning" : "passing"
return (
<div className={`budget-card status-${status}`}>
<div className="metric-name">{metric}</div>
<div className="current-value">{formatMetric(metric, current)}</div>
<div className="budget-bar">
<div className="bar-fill" style={{ width: `${Math.min(ratio * 100, 100)}%` }} />
</div>
<div className="budget-limit">Limit: {formatMetric(metric, budget)}</div>
</div>
)
}
预算维护:何时调整
| 信号 | 行动 |
|---|---|
| 预算持续失败且超幅 < 10% | 将预算降低 5%,与实际性能匹配 |
| 预算持续失败且超幅 > 20% | 追查根本原因——修复后再调整预算 |
| 预算通过且余量 > 50% | 收紧预算——可以更激进一些 |
| 新功能需要增加预算 | 增加预算,同时设定优化回落日期 |
| Core Web Vitals 排名下降 | 追查并修复——不要通过调整预算来迁就 |
团队协作流程
开发者提交 PR
→ CI 运行资源包分析 + Lighthouse
→ 所有预算通过 → 自动合并开启
→ 任意预算失败 → PR 评论列出具体失败项
→ 开发者审查并优化
→ 或者:确认性能退化,添加 performance-debt 标签
→ 上线前周五进行性能评审
性能预算实施清单
- 定义 JS(首屏)、CSS 和各路由分片的资源包大小预算
- 定义图片预算(总页面重量、最多图片数)
- 定义时序预算(LCP、CLS、INP、TTFB)
- CI 流水线在每次 PR 时执行预算检查
- 构建产物中集成资源包分析器
- 每次 PR 运行 Lighthouse CI 并配置断言失败
- 在监控工具中配置性能预算仪表板
- 生产环境预算超限时发送自动化告警
- 每季度审查并调整预算
- 在 GitHub 中为已确认的性能退化添加标签
结语
性能预算不是为了追求完美——而是为了防止性能静默退化。通过定义明确的阈值并在 CI 中强制执行,你可以在性能退化进入生产环境之前就将其捕获。
核心洞察:每个功能都有性能成本。在代码审查阶段而非用户投诉之后承认这一成本,将性能从亡羊补牢转变为第一优先级的设计约束。
TanStack Start 较小的基础资源包大小(约 85 KB)为性能建设提供了良好起点,但真正的性能表现需要持续的执行纪律。如需了解更多基础指标和优化技术,请参阅我们的 Core Web Vitals 优化指南、真实用户监控(用于生产环境测量)、Lighthouse 95+ 优化技巧(审计最佳实践)以及 边缘缓存策略(CDN 层性能提升)。