Skip to content
Back to Quality Assurance

Performance Budget

Set limits on bundle size, image size, and request count, and enforce them in CI to prevent regressions.

Why

A performance budget is a contract: "this page will ship no more than 200KB of JavaScript, 500KB of images, and make no more than 20 requests." Without it, every PR adds a little — a new dependency here, an extra image there — and the page grows from 1s to 4s over a year with no single change to blame. The budget makes every kilobyte a deliberate decision: when a PR adds 50KB, the developer must either justify it, optimize something else to make room, or reduce the addition. This is the only mechanism that prevents the slow, silent performance death that every team without a budget eventually suffers. The budget is set once, enforced in CI, and reviewed periodically — it turns performance from a reactive fire drill into a proactive guardrail.

When

Set the budget at project start or when performance becomes a problem. Enforce on every PR. Review quarterly to tighten as the team improves or loosen if the budget is blocking legitimate features.

How

1. Measure the current performance to set a realistic baseline
   - Run Lighthouse on the key pages, record the current bundle/image/request counts
   - Set the budget at 110% of current — tight enough to catch regressions, loose enough to not block normal work
   - As the team optimizes, tighten the budget toward the target (e.g., 200KB JS)

2. Define the budget in a machine-readable format
   - Bundle size: size-limit or bundlesize in package.json
   - Lighthouse scores: lighthouserc.json with assertions
   - Resource counts: custom CI step that counts <script> and <img> tags

3. Enforce the budget in CI on every PR
   - Run size-limit and lighthouse-ci as CI steps
   - Fail the build if any budget is exceeded
   - Post a comment on the PR showing the size delta (before → after)

4. When a PR exceeds the budget, the developer must:
   - Optimize: reduce the size elsewhere to make room (tree-shake, code-split, compress)
   - Justify: add a comment explaining why the increase is necessary and acceptable
   - Increase the budget: update the budget file with review (last resort, not routine)

5. Track the budget over time
   - Plot the actual bundle size and Lighthouse scores on a dashboard
   - Annotate deploys — see which PRs grew the bundle and which shrank it
   - Celebrate when the team shrinks the budget — make performance wins visible

Example

// package.json — size-limit budget
{
  "size-limit": [
    { "path": "dist/main.js", "limit": "200 KB", "gzip": true },
    { "path": "dist/vendor.js", "limit": "150 KB", "gzip": true },
    { "path": "dist/styles.css", "limit": "30 KB", "gzip": true }
  ]
}

// lighthouserc.json — performance budget
{
  "ci": {
    "assert": {
      "assertions": {
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["warn", { "maxNumericValue": 300 }],
        "resource-summary:script:size": ["error", { "maxNumericValue": 200000 }],
        "resource-summary:image:size": ["error", { "maxNumericValue": 500000 }],
        "resource-summary:script:count": ["warn", { "maxNumericValue": 15 }]
      }
    }
  }
}

// CI: .github/workflows/budget.yml
name: Performance Budget
on: [pull_request]
jobs:
  budget:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - name: Check bundle size
        run: npx size-limit
      - name: Check Lighthouse budget
        run: npx @lhci/cli autorun
      - name: Comment size delta on PR
        uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          # Posts: "Bundle size: 198 KB → 205 KB (+7 KB) ⚠️ Budget: 200 KB"

Common Problems

  • ⚠Setting a budget but not enforcing it in CI — the budget is documented in a wiki, nobody checks it, and the bundle grows past the limit with every PR because there is no automated gate.
  • ⚠Setting the budget too tight (at the current size) blocks every PR that adds a feature, frustrating developers and leading to the budget being relaxed repeatedly until it is meaningless.
  • ⚠Budgeting only JavaScript and ignoring images, fonts, and CSS — the JS bundle is 150KB (under budget) but the page ships 3MB of unoptimized images, and the budget gives a false sense of control.
  • ⚠Enforcing the budget as a warning instead of an error — warnings are ignored in CI, the build passes, and the regression ships to production where users experience the slowdown.

Best Practices

  • ✓Set the budget at 110% of the current size initially, then tighten over time — this catches regressions without blocking normal work, and the team can ratchet down as they optimize.
  • ✓Budget all resource types (JS, CSS, images, fonts) and all performance metrics (LCP, CLS, TBT) — a budget that covers only one dimension gives a false sense of control over the others.
  • ✓Enforce the budget as a CI error, not a warning — a warning is ignored, an error forces the developer to address the regression before merge, which is the entire point of the budget.
  • ✓Post the size delta as a PR comment (before → after → budget) so the developer sees the impact of their change immediately — visibility at PR time prevents the regression from being merged.