Performance Budgets with Lighthouse CI

Set and enforce web performance budgets using Lighthouse CI assertions. Learn presets, custom thresholds, and budget.json configuration.
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.

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
module.exports = {
  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:

module.exports = {
  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:

module.exports = {
  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:

module.exports = {
  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:

module.exports = {
  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:

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):

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:

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.

module.exports = {
  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:

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:

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):

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.

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

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

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

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

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:

module.exports = {
  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:

module.exports = {
  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:

// 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:

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

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

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

Move from warn to error as team adapts.

Practical Examples

Marketing Site

module.exports = {
  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

module.exports = {
  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

module.exports = {
  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

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