Performance Budgets with Lighthouse CI
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:
| Finding | Source |
|---|---|
| 0.1s improvement = 8.4% retail conversion increase | Google/Deloitte "Milliseconds Make Millions" |
| Every 100ms costs Amazon 1% in sales | Cloudflare |
| BBC loses 10% of users per additional second | Hobo Web |
| Bounce probability increases 90% as load goes 1s→5s | Google Research |
| Good LCP = 61% conversion increase for Rakuten | web.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.
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.
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 bundlesstylesheet- CSS filesimage- All image formatsmedia- Video/audiofont- Web fontsdocument- HTML documentsother- Everything elsethird-party- External domain resourcestotal- Combined size
Timing Metrics
Available metric values:
first-contentful-paintlargest-contentful-paintinteractivetotal-blocking-timecumulative-layout-shiftmax-potential-fidspeed-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
- Largest Contentful Paint (LCP) - Slow loading of main content
- Total byte weight - Excessive page resources
- Unused JavaScript - Dead code bloating bundles
- Unminified JavaScript - Missing compression
- Cumulative Layout Shift (CLS) - Visual instability and layout shifts
- Interaction to Next Paint (INP) - Poor interactivity and responsiveness
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
- JavaScript size violations: Check unused JavaScript and unminified JavaScript
- Total size violations: Review total byte weight optimization strategies
- Image budgets: Optimize formats, implement responsive images, lazy loading
Start with Core Web Vitals guide for prioritized optimization strategies.
Next Steps
- Configuration - Full lighthouserc.js options
- GitHub Actions - Automate budgets in CI
- GitLab CI - GitLab CI integration