Performance Budgets with Lighthouse CI: Assertions & budget.json

Set and enforce performance budgets with Lighthouse CI. Configure assertions, presets, custom thresholds, and budget.json to prevent regressions in CI/CD.
Harlan WiltonHarlan Wilton Published

Performance budgets enforce web performance standards in your CI pipeline. Lighthouse CI fails builds when metrics exceed thresholds, preventing performance regressions from reaching production.

Why Performance Budgets Matter

Performance budgets translate business requirements into measurable technical constraints:

  • Prevent regressions: Catch performance degradation before deployment
  • Objective standards: Replace subjective "feels slow" with measurable thresholds
  • Team accountability: Make performance a first-class concern alongside features
  • User experience: Maintain fast load times as codebases grow

Without budgets, performance degrades incrementally. A 50KB script here, an unoptimized image there - each change seems minor, but compound effects destroy user experience.

The Business Case

Research shows direct revenue impact from performance:

FindingSource
0.1s improvement = 8.4% retail conversion increaseGoogle/Deloitte "Milliseconds Make Millions"
Every 100ms costs Amazon 1% in salesCloudflare
BBC loses 10% of users per additional secondHobo Web
Bounce probability increases 90% as load goes 1s→5sGoogle Research
Good LCP = 61% conversion increase for Rakutenweb.dev Case Study

The Page Weight Problem

Page weight has grown 356% in a decade (484KB → 2.2MB average). The 2024 Web Almanac reports median page weight at 2,675 KB (+8% YoY) with median JavaScript at 558 KB on mobile.

Alex Russell's research suggests a 365 KB JavaScript budget for sub-3-second loads on typical mobile devices — yet the 75th percentile exceeds 650 KB. Before setting budgets, measure your current baseline with the Page Size Checker.

Two Approaches to Budgets

Lighthouse CI supports two budget methods:

1. Assertions in lighthouserc.js

Define budgets directly in your Lighthouse CI configuration:

// lighthouserc.js
export default {
  ci: {
    assert: {
      preset: 'lighthouse:recommended',
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
      }
    }
  }
}

Best for simple budgets integrated with CI configuration.

2. Separate budget.json

Use Lighthouse's native budget format:

// budget.json
[
  {
    "path": "/*",
    "resourceSizes": [
      { "resourceType": "script", "budget": 300 },
      { "resourceType": "image", "budget": 500 }
    ],
    "timings": [
      { "metric": "interactive", "budget": 3000 }
    ]
  }
]

Reference it in lighthouserc.js:

const config2 = {
  ci: {
    assert: {
      budgetsFile: './budget.json'
    }
  }
}

Best for complex budgets, especially multiple URL patterns or resource-level constraints.

Assertion Presets

Lighthouse CI includes three presets based on common use cases.

lighthouse:all

Strictest preset - every audit must pass:

const config3 = {
  ci: {
    assert: {
      preset: 'lighthouse:all'
    }
  }
}

Enforces all 100+ Lighthouse audits. Extremely difficult to pass. Use for greenfield projects with aggressive performance goals.

lighthouse:recommended

Balanced preset - focuses on critical metrics:

const config4 = {
  ci: {
    assert: {
      preset: 'lighthouse:recommended'
    }
  }
}

Behavior:

  • Errors when accessibility, best practices, or SEO audits fail (perfect scores required)
  • Warns when performance metrics fall below a score of 90
  • Covers critical audits without being overly strict on performance variance

Most common starting point. Reasonable for production sites.

lighthouse:no-pwa

Recommended preset without PWA requirements:

const config5 = {
  ci: {
    assert: {
      preset: 'lighthouse:no-pwa'
    }
  }
}

Same as lighthouse:recommended but excludes Progressive Web App audits. Use for standard websites that don't need PWA capabilities.

Assertion Syntax

Lighthouse CI uses ESLint-style assertion levels.

Assertion Levels

Three severity levels control build status:

const config6 = {
  assertions: {
    'first-contentful-paint': 'off', // Ignore this audit
    'largest-contentful-paint': 'warn', // Log warning, don't fail build
    'cumulative-layout-shift': 'error' // Fail build if threshold exceeded
  }
}
  • off: Skip assertion entirely
  • warn: Report violations without failing build
  • error: Fail build on violations (default for preset assertions)

minScore vs maxNumericValue

Two threshold types handle different metric formats:

minScore for 0-1 scores (categories, some audits):

const config7 = {
  assertions: {
    'categories:performance': ['error', { minScore: 0.9 }],
    'categories:accessibility': ['error', { minScore: 0.95 }],
    'categories:seo': ['warn', { minScore: 0.8 }]
  }
}

Score must meet or exceed threshold. 0.9 means 90/100 in Lighthouse reports.

maxNumericValue for millisecond/byte values:

const config8 = {
  assertions: {
    'first-contentful-paint': ['error', { maxNumericValue: 1800 }],
    'speed-index': ['error', { maxNumericValue: 3000 }],
    'total-byte-weight': ['warn', { maxNumericValue: 1500000 }]
  }
}

Value must be at or below threshold. FCP example allows up to 1800ms.

Aggregation Methods

Lighthouse CI runs multiple audits per URL. Aggregation determines which run's value to compare against budgets.

const config9 = {
  ci: {
    assert: {
      assertions: { /* ... */ }
    },
    collect: {
      numberOfRuns: 5
    },
    upload: {
      target: 'temporary-public-storage'
    }
  }
}

Four aggregation strategies:

optimistic (default)

Uses best (fastest) value from sorted runs. Presets like lighthouse:recommended use this by default.

median

Uses middle value from sorted runs:

const config10 = {
  assert: {
    assertMatrix: [{ matchingUrlPattern: '.*', assertions: { /* ... */ } }],
    // median is default, explicit:
    preset: 'lighthouse:recommended'
  }
}

Most stable - ignores outliers. Best for typical use when you want consistent results.

pessimistic

Uses worst (slowest) run:

const config11 = {
  assert: {
    assertions: {
      'largest-contentful-paint': ['error', {
        maxNumericValue: 2500,
        aggregationMethod: 'pessimistic'
      }]
    }
  }
}

Strictest - ensures worst case meets budget. Good for critical paths.

median-run

Uses all values from the median run (not per-metric median):

const config12 = {
  assert: {
    assertions: {
      interactive: ['error', {
        maxNumericValue: 3500,
        aggregationMethod: 'median-run'
      }]
    }
  }
}

Ensures metric correlations preserved. Use when metrics interrelate.

Category Assertions

Category assertions set thresholds for Lighthouse's five main scores.

const config13 = {
  assertions: {
    'categories:performance': ['error', { minScore: 0.9 }],
    'categories:accessibility': ['error', { minScore: 0.95 }],
    'categories:best-practices': ['error', { minScore: 0.9 }],
    'categories:seo': ['error', { minScore: 0.9 }],
    'categories:pwa': ['warn', { minScore: 0.5 }]
  }
}

Categories aggregate multiple audits. Performance category includes FCP, LCP, TBT, CLS, and Speed Index.

Use categories for high-level budgets. Override with individual audit assertions for granular control.

Individual Audit Assertions

Target specific Lighthouse audits for precise budgets.

Core Web Vitals

const config14 = {
  assertions: {
    'first-contentful-paint': ['error', { maxNumericValue: 1800 }],
    'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
    'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
    'total-blocking-time': ['error', { maxNumericValue: 200 }],
    'speed-index': ['error', { maxNumericValue: 3400 }]
  }
}

Align with Google's Core Web Vitals thresholds for good user experience. See detailed guides for LCP, CLS, and INP optimization.

Lab scores ≠ field performance: Calibre's research found that 43% of pages scoring 90+ in Lighthouse failed one or more Core Web Vitals thresholds in real-world CrUX data. Use RUM monitoring alongside synthetic testing.

Resource Metrics

const config15 = {
  assertions: {
    'total-byte-weight': ['error', { maxNumericValue: 1600000 }], // 1.6MB
    'dom-size': ['warn', { maxNumericValue: 1500 }],
    'bootup-time': ['warn', { maxNumericValue: 3500 }],
    'mainthread-work-breakdown': ['warn', { maxNumericValue: 4000 }]
  }
}

Control page weight and computational cost. High total byte weight directly impacts load performance.

Resource Budgets by Type

const config16 = {
  assertions: {
    'resource-summary:script:size': ['error', { maxNumericValue: 300000 }], // 300KB JS
    'resource-summary:stylesheet:size': ['error', { maxNumericValue: 100000 }], // 100KB CSS
    'resource-summary:image:size': ['warn', { maxNumericValue: 500000 }], // 500KB images
    'resource-summary:font:size': ['warn', { maxNumericValue: 100000 }], // 100KB fonts
    'resource-summary:total:size': ['error', { maxNumericValue: 1500000 }] // 1.5MB total
  }
}

Granular control over asset budgets. Prevents JavaScript bloat from unused JavaScript or unminified scripts.

Recommended JS budget: Alex Russell's performance inequality research suggests 365 KB total JavaScript for sub-3-second loads on median mobile devices. The 300KB budget above is aggressive but achievable for content sites.

Accessibility Audits

const config17 = {
  assertions: {
    'color-contrast': 'error',
    'image-alt': 'error',
    'label': 'error',
    'link-name': 'error',
    'tabindex': 'warn'
  }
}

Binary pass/fail audits don't need thresholds. See the accessibility guide for comprehensive audit details.

budget.json Format

Lighthouse's native budget format supports per-path budgets.

Basic Structure

[
  {
    "path": "/*",
    "resourceSizes": [
      {
        "resourceType": "script",
        "budget": 300
      },
      {
        "resourceType": "stylesheet",
        "budget": 50
      },
      {
        "resourceType": "image",
        "budget": 500
      },
      {
        "resourceType": "font",
        "budget": 100
      },
      {
        "resourceType": "total",
        "budget": 1500
      }
    ],
    "timings": [
      {
        "metric": "first-contentful-paint",
        "budget": 2000
      },
      {
        "metric": "largest-contentful-paint",
        "budget": 2500
      },
      {
        "metric": "interactive",
        "budget": 3500
      },
      {
        "metric": "total-blocking-time",
        "budget": 200
      }
    ]
  }
]

Resource Types

Available resourceType values:

  • script - JavaScript bundles
  • stylesheet - CSS files
  • image - All image formats
  • media - Video/audio
  • font - Web fonts
  • document - HTML documents
  • other - Everything else
  • third-party - External domain resources
  • total - Combined size

Timing Metrics

Available metric values:

  • first-contentful-paint
  • largest-contentful-paint
  • interactive
  • total-blocking-time
  • cumulative-layout-shift
  • max-potential-fid
  • speed-index

Multiple URL Budgets

Define different budgets per page type:

[
  {
    "path": "/",
    "resourceSizes": [
      { "resourceType": "script", "budget": 400 },
      { "resourceType": "total", "budget": 2000 }
    ]
  },
  {
    "path": "/blog/*",
    "resourceSizes": [
      { "resourceType": "script", "budget": 200 },
      { "resourceType": "image", "budget": 800 }
    ]
  },
  {
    "path": "/app/*",
    "resourceSizes": [
      { "resourceType": "script", "budget": 600 },
      { "resourceType": "total", "budget": 1800 }
    ]
  }
]

Match patterns use glob syntax. First matching path applies.

Using budget.json

Reference budget file in lighthouserc.js:

const config18 = {
  ci: {
    assert: {
      budgetsFile: './budget.json'
    },
    collect: {
      url: [
        'http://localhost:3000/',
        'http://localhost:3000/blog/post-1',
        'http://localhost:3000/app/dashboard'
      ]
    }
  }
}

Budget.json takes precedence over assertion configuration.

Combining Presets with Overrides

Start with preset, override specific assertions:

const config19 = {
  ci: {
    assert: {
      preset: 'lighthouse:no-pwa',
      assertions: {
        // Relax performance requirement
        'categories:performance': ['warn', { minScore: 0.85 }],

        // Stricter Core Web Vitals
        'largest-contentful-paint': ['error', { maxNumericValue: 2000 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.05 }],

        // Resource budgets
        'resource-summary:script:size': ['error', { maxNumericValue: 350000 }],
        'resource-summary:total:size': ['error', { maxNumericValue: 1800000 }],

        // Disable problematic audits
        'uses-http2': 'off',
        'redirects': 'off'
      }
    }
  }
}

Presets provide baseline. Overrides customize for your application.

Setting Realistic Budgets

Don't guess budgets. Base them on current performance:

1. Run Lighthouse CI Locally

lhci collect --url=http://localhost:3000
lhci assert --preset=lighthouse:recommended

Review failures to see current metric values.

2. Set Budgets Slightly Above Current

Add 10-20% buffer to current values:

const config20 = {
  // Current FCP: 1600ms
  // Budget: 1600 * 1.15 = 1840ms
  'first-contentful-paint': ['error', { maxNumericValue: 1840 }]
}

Prevents immediate failures while preventing regressions.

3. Tighten Over Time

Gradually reduce budgets as performance improves:

const config21 = {
  // Week 1
  'largest-contentful-paint': ['warn', { maxNumericValue: 3000 }]
}

const config22 = {
  // Week 4
  'largest-contentful-paint': ['warn', { maxNumericValue: 2500 }]
}

const config23 = {
  // Week 8
  'largest-contentful-paint': ['error', { maxNumericValue: 2500 }]
}

Move from warn to error as team adapts.

Practical Examples

Marketing Site

const config24 = {
  ci: {
    assert: {
      preset: 'lighthouse:no-pwa',
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'categories:accessibility': ['error', { minScore: 0.95 }],
        'categories:seo': ['error', { minScore: 0.95 }],

        'first-contentful-paint': ['error', { maxNumericValue: 1500 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2000 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.05 }],

        'resource-summary:script:size': ['error', { maxNumericValue: 250000 }],
        'resource-summary:image:size': ['warn', { maxNumericValue: 600000 }]
      }
    }
  }
}

Priorities: SEO, accessibility, fast LCP for conversions.

SaaS Application

const config25 = {
  ci: {
    assert: {
      preset: 'lighthouse:no-pwa',
      assertions: {
        'categories:performance': ['warn', { minScore: 0.8 }],
        'categories:accessibility': ['error', { minScore: 0.9 }],

        'interactive': ['error', { maxNumericValue: 3500 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }],

        'resource-summary:script:size': ['warn', { maxNumericValue: 500000 }],
        'bootup-time': ['warn', { maxNumericValue: 4000 }]
      }
    }
  }
}

Priorities: Interactivity, reasonable bundle size for complex UI.

Content Site

const config26 = {
  ci: {
    assert: {
      budgetsFile: './budget.json'
    }
  }
}
[
  {
    "path": "/",
    "resourceSizes": [
      { "resourceType": "script", "budget": 200 },
      { "resourceType": "image", "budget": 400 },
      { "resourceType": "total", "budget": 1200 }
    ],
    "timings": [
      { "metric": "first-contentful-paint", "budget": 1500 },
      { "metric": "largest-contentful-paint", "budget": 2000 }
    ]
  },
  {
    "path": "/article/*",
    "resourceSizes": [
      { "resourceType": "script", "budget": 150 },
      { "resourceType": "image", "budget": 800 },
      { "resourceType": "total", "budget": 1500 }
    ],
    "timings": [
      { "metric": "first-contentful-paint", "budget": 1200 },
      { "metric": "largest-contentful-paint", "budget": 1800 }
    ]
  }
]

Priorities: Fast content delivery, article images allowed, minimal JavaScript.

E-commerce Site

const config27 = {
  module: {
    exports: {
      ci: {
        assert: {
          preset: 'lighthouse:no-pwa',
          assertions: {
            'categories:performance': ['error', { minScore: 0.85 }],
            'categories:accessibility': ['error', { minScore: 0.9 }],

            'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
            'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
            'total-blocking-time': ['warn', { maxNumericValue: 350 }],

            'resource-summary:script:size': ['warn', { maxNumericValue: 400000 }],
            'resource-summary:image:size': ['error', { maxNumericValue: 1000000 }],
            'resource-summary:third-party:size': ['warn', { maxNumericValue: 300000 }]
          }
        }
      }
    }
  }
}

Priorities: LCP (product images), CLS (prevent layout shifts during checkout), third-party control (analytics/ads).

When Budgets Fail

Budget violations point to specific performance problems. These diagnostic guides help identify root causes and implement fixes:

Core Web Vitals Failures

Category Score Failures

  • Accessibility - WCAG violations, missing labels, color contrast
  • SEO - Meta tags, structured data, mobile usability
  • Best Practices - Security, browser compatibility, console errors

Resource Budget Failures

Start with Core Web Vitals guide for prioritized optimization strategies.

Next Steps