What Are Performance Budgets?
A performance budget is a set of quantitative limits on metrics that affect user experience. Budgets transform performance from a vague aspiration into a measurable, enforceable standard. By setting budgets on bundle size, load time, and Core Web Vitals, teams can catch regressions before they reach production and make informed trade-off decisions.
Recommended Budget Targets
- Total JavaScript: <300KB gzipped — Includes all first-party and third-party JS
- Total page weight: <1.5MB — All resources including images, fonts, scripts
- LCP: <2.5 seconds — Largest content visible within 2.5s on 4G
- INP: <200ms — All interactions respond within 200ms
- CLS: <0.1 — Minimal unexpected layout shifts
- Time to Interactive: <3.8 seconds — Page fully interactive on mobile 4G
Implementing Budget Checks
// performance-budget.config.ts
interface PerformanceBudget {
metric: string;
budget: number;
unit: string;
severity: 'error' | 'warning';
}
export const budgets: PerformanceBudget[] = [
// Bundle size budgets
{ metric: 'total-js-size', budget: 300, unit: 'KB (gzipped)', severity: 'error' },
{ metric: 'total-css-size', budget: 50, unit: 'KB (gzipped)', severity: 'warning' },
{ metric: 'largest-chunk', budget: 100, unit: 'KB (gzipped)', severity: 'error' },
{ metric: 'total-image-size', budget: 500, unit: 'KB', severity: 'warning' },
// Lighthouse budgets
{ metric: 'performance-score', budget: 90, unit: 'score', severity: 'error' },
{ metric: 'lcp', budget: 2500, unit: 'ms', severity: 'error' },
{ metric: 'cls', budget: 0.1, unit: 'score', severity: 'error' },
{ metric: 'tbt', budget: 300, unit: 'ms', severity: 'warning' },
{ metric: 'fcp', budget: 1800, unit: 'ms', severity: 'warning' },
// Request count budgets
{ metric: 'total-requests', budget: 50, unit: 'requests', severity: 'warning' },
{ metric: 'third-party-requests', budget: 10, unit: 'requests', severity: 'warning' },
];// check-budgets.ts — CI script to enforce budgets
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
interface BudgetResult {
metric: string;
actual: number;
budget: number;
unit: string;
passed: boolean;
severity: string;
}
function checkBundleSizeBudgets(buildDir: string): BudgetResult[] {
const results: BudgetResult[] = [];
const staticDir = path.join(buildDir, '.next', 'static');
// Calculate total JS size (gzipped)
let totalJsSize = 0;
let largestChunk = 0;
function processDir(dir: string): void {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
processDir(fullPath);
} else if (entry.name.endsWith('.js')) {
const content = fs.readFileSync(fullPath);
const gzipped = zlib.gzipSync(content);
const sizeKB = gzipped.length / 1024;
totalJsSize += sizeKB;
largestChunk = Math.max(largestChunk, sizeKB);
}
}
}
processDir(staticDir);
results.push({
metric: 'total-js-size',
actual: Math.round(totalJsSize),
budget: 300,
unit: 'KB (gzipped)',
passed: totalJsSize <= 300,
severity: 'error',
});
results.push({
metric: 'largest-chunk',
actual: Math.round(largestChunk),
budget: 100,
unit: 'KB (gzipped)',
passed: largestChunk <= 100,
severity: 'error',
});
return results;
}
// Run checks
const results = checkBundleSizeBudgets(process.cwd());
const errors = results.filter(r => !r.passed && r.severity === 'error');
const warnings = results.filter(r => !r.passed && r.severity === 'warning');
console.log('\nPerformance Budget Report:');
console.log('='.repeat(60));
for (const r of results) {
const icon = r.passed ? 'PASS' : r.severity === 'error' ? 'FAIL' : 'WARN';
console.log(`[${icon}] ${r.metric}: ${r.actual} / ${r.budget} ${r.unit}`);
}
if (errors.length > 0) {
console.error(`\n${errors.length} budget(s) exceeded! Build failed.`);
process.exit(1);
}
if (warnings.length > 0) {
console.warn(`\n${warnings.length} budget warning(s).`);
}GitHub Action for Budget Enforcement
// .github/workflows/perf-budget.yml
// name: Performance Budget Check
// on: [pull_request]
// jobs:
// budget-check:
// runs-on: ubuntu-latest
// steps:
// - uses: actions/checkout@v4
// - uses: actions/setup-node@v4
// with: { node-version: 20 }
// - run: npm ci
// - run: npm run build
// - name: Check bundle budgets
// run: npx tsx scripts/check-budgets.ts
// - name: Run Lighthouse
// uses: treosh/lighthouse-ci-action@v11
// with:
// configPath: './lighthouserc.js'
// uploadArtifacts: true
// PR comment with budget diff
import { Octokit } from '@octokit/rest';
async function commentBudgetReport(
results: BudgetResult[],
prNumber: number
): Promise<void> {
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const table = results.map(r => {
const status = r.passed ? ':white_check_mark:' : ':x:';
return `| ${status} | ${r.metric} | ${r.actual} | ${r.budget} | ${r.unit} |`;
}).join('\n');
const body = `## Performance Budget Report\n
| Status | Metric | Actual | Budget | Unit |
|--------|--------|--------|--------|------|
${table}
\n_Generated by Performance Budget CI_`;
await octokit.issues.createComment({
owner: 'your-org',
repo: 'your-repo',
issue_number: prNumber,
body,
});
}Performance Budget Tips
- Start from current baseline: Measure your current performance and set budgets slightly below
- Enforce in CI: Fail builds that exceed error-level budgets
- Track trends: Monitor budget metrics over time to catch gradual degradation
- Review regularly: Tighten budgets as you optimize, loosen if justified by features
- Make budgets visible: Post results on PR comments so the entire team sees them