# Building a Performance Budget That Actually Gets Enforced
By Govind Bajaj · 2026

Tags: performance, ci-cd, web-performance, testing, automation
Performance budgets fail because they are spreadsheets, not tests. I moved mine into CI. Now builds fail when bundles grow. Here's the setup.
Every performance optimization I have done eventually regressed.

The hero image got optimized, then someone uploaded a new one without compression. The bundle was code-split, then a developer imported a 200KB library in a shared utility file. The font was preloaded, then a marketing tag manager added a render-blocking script.

Performance budgets written in spreadsheets or documentation do not prevent regression. Performance budgets enforced in CI do.

## What a Performance Budget Actually Is

A performance budget is a set of limits on metrics that affect user experience. My standard budget for e-commerce sites:

Metric Budget Alert Fail LCP less than or equal to 2.0s greater than 2.3s greater than 2.5s INP less than or equal to 150ms greater than 180ms greater than 200ms CLS less than or equal to 0.05 greater than 0.08 greater than 0.1 Total JS less than or equal to 200KB gzipped greater than 230KB greater than 250KB Images less than or equal to 500KB total greater than 600KB greater than 700KB Third-party JS less than or equal to 100KB greater than 120KB greater than 150KB

The budget column is what I optimize for. The alert column triggers a warning in CI. The fail column blocks the build.

## Bundle Size: webpack-bundle-analyzer + CI

Install the analyzer:

```bash
npm install -D @next/bundle-analyzer
```

Add a CI script that fails on budget exceeded:

```js
// scripts/check-bundle-size.js
const fs = require('fs')
const path = require('path')

const BUDGET = 200 * 1024 // 200KB
const buildManifest = require('../.next/build-manifest.json')

const pages = buildManifest.pages
let failed = false

for (const [page, files] of Object.entries(pages)) {
  const totalSize = files
    .filter(f => f.endsWith('.js'))
    .map(f => fs.statSync(path.join('.next', f)).size)
    .reduce((a, b) => a + b, 0)
  
  if (totalSize > BUDGET) {
    console.error(
      `FAIL: ${page} is ${(totalSize / 1024).toFixed(1)}KB (budget: ${BUDGET / 1024}KB)`
    )
    failed = true
  }
}

process.exit(failed ? 1 : 0)
```

Add to your CI pipeline:

```yaml
# .github/workflows/performance.yml
name: Performance Budget
on: [pull_request]
jobs:
  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
      - run: node scripts/check-bundle-size.js
```

## Lighthouse CI for Real Metrics

```bash
npm install -D @lhci/cli
```

```json
{
  "ci": {
    "collect": {
      "url": ["http://localhost:3000/", "http://localhost:3000/products"],
      "startServerCommand": "npm start"
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", {"minScore": 0.9}],
        "largest-contentful-paint": ["error", {"maxNumericValue": 2500}],
        "cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}]
      }
    }
  }
}
```

## Size-Limit for PR Comments

The size-limit package adds bundle size comments to pull requests. This comments on every PR with the bundle size change, making regressions visible before merge.

## The Human Process

Tools enforce budgets, but you need a process for when budgets are hit:

1. Alert level: PR can merge with team approval. Document the exception.
2. Fail level: PR cannot merge. The developer must optimize or increase the budget with justification.
3. Quarterly review: Revisit budgets. As features grow, budgets may need adjustment.

## Takeaways

- Performance budgets in spreadsheets are wishlists — budgets in CI are enforceable
- Check bundle sizes with a post-build script that exits with code 1 on violation
- Lighthouse CI audits real performance metrics on every PR
- Size-limit adds bundle size comments to GitHub PRs for visibility
- Set both alert (warning) and fail (blocking) thresholds
- Review budgets quarterly — they should tighten, not loosen, over time