Web Vitals Budget Management
What This Means
A Web Vitals budget is a set of performance thresholds you establish for Core Web Vitals (LCP, INP, CLS) and other metrics to ensure your site maintains optimal user experience. Think of it like a financial budget - you allocate resources (loading time, script execution, layout shifts) across different page elements while staying within acceptable limits.
Core Web Vitals Thresholds
Good Performance (Green):
- LCP (Largest Contentful Paint): ≤ 2.5 seconds
- INP (Interaction to Next Paint): ≤ 200 milliseconds
- CLS (Cumulative Layout Shift): ≤ 0.1
Needs Improvement (Yellow):
- LCP: 2.5 - 4.0 seconds
- INP: 200 - 500 milliseconds
- CLS: 0.1 - 0.25
Poor Performance (Red):
- LCP: > 4.0 seconds
- INP: > 500 milliseconds
- CLS: > 0.25
Why Budgets Matter
Performance Governance:
- Prevents performance degradation over time
- Establishes accountability for page speed
- Enables data-driven decisions about features
- Creates performance culture in development
Business Impact:
- 100ms faster LCP = 1% increase in conversion (varies by industry)
- Sites meeting Core Web Vitals see 24% less abandonment
- Good INP scores correlate with higher engagement
- CLS issues directly reduce trust and conversions
SEO Benefits:
- Core Web Vitals are Google ranking factors
- Better metrics = better search visibility
- Improved mobile search rankings
- Enhanced user signals (lower bounce rates)
How to Diagnose
Method 1: Chrome User Experience Report (CrUX)
Check Real User Metrics:
- Visit PageSpeed Insights
- Enter your URL
- Review "Discover what your real users are experiencing" section
- Check metrics at 75th percentile (Google's threshold)
What to Look For:
- Green checkmarks for all Core Web Vitals
- Field data from actual users (not lab data)
- Mobile vs desktop differences
- Origin-level vs page-level metrics
- Percentage of "good" experiences
Method 2: Google Search Console
- Navigate to Google Search Console
- Click "Core Web Vitals" in left sidebar
- Review "Poor," "Need Improvement," and "Good" URLs
- Identify which pages fail thresholds
What to Look For:
- Number of URLs in each category
- Specific pages with issues
- Mobile vs desktop performance gaps
- Trends over time
- Impact on total impressions
Method 3: Lighthouse Performance Budget
- Open Chrome DevTools (
F12) - Navigate to "Lighthouse" tab
- Click "⚙️" (settings gear)
- Enable "Performance budget"
- Configure budgets for:
Example Budget Configuration:
{
"resourceSizes": [
{
"resourceType": "script",
"budget": 300
},
{
"resourceType": "stylesheet",
"budget": 50
},
{
"resourceType": "image",
"budget": 500
},
{
"resourceType": "total",
"budget": 1000
}
],
"timings": [
{
"metric": "interactive",
"budget": 3000
},
{
"metric": "first-contentful-paint",
"budget": 1800
}
]
}
Method 4: Web Vitals JavaScript Library
Measure Real User Performance:
<script type="module">
import {onCLS, onINP, onLCP} from 'https://unpkg.com/web-vitals@3?module';
const vitalsUrl = 'https://your-analytics-endpoint.com/vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
// Check against budget thresholds
const budgets = {
LCP: 2500,
INP: 200,
CLS: 0.1
};
const status = metric.value <= budgets[metric.name] ? 'good' : 'poor';
console.log(`${metric.name}: ${metric.value} (${status})`);
// Send to your analytics
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, body);
}
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
</script>
Method 5: WebPageTest Performance Budgets
- Visit WebPageTest.org
- Enter your URL
- Click "Advanced Settings"
- Scroll to "Performance Budget"
- Set thresholds for:
- Speed Index
- LCP
- TBT (Total Blocking Time)
- CLS
- Resource sizes
What to Look For:
- Tests that fail budget requirements
- Waterfall chart showing bottlenecks
- Filmstrip view of loading experience
- Specific resources exceeding budget
General Fixes
Fix 1: Establish Component-Level Budgets
Break down page budget by component:
Header/Navigation:
- LCP contribution: < 500ms
- JavaScript: < 50KB
- Images: < 100KB
- CLS contribution: 0
Hero/Above-Fold Content:
- LCP contribution: < 1500ms (this is your LCP element)
- JavaScript: < 100KB
- Images: < 300KB (use modern formats)
- CLS contribution: 0
Main Content:
- INP budget: < 100ms per interaction
- JavaScript: < 150KB
- Total size: < 500KB
- CLS contribution: < 0.05
Third-Party Scripts:
- Total budget: < 200KB
- Execution time: < 300ms
- No render-blocking
- Load after critical content
Example Budget Spreadsheet:
| Component | JS Budget | CSS Budget | Image Budget | LCP Impact | CLS Budget |
|---|---|---|---|---|---|
| Header | 50KB | 20KB | 100KB | 500ms | 0 |
| Hero | 100KB | 30KB | 300KB | 1500ms | 0 |
| Content | 150KB | 40KB | 500KB | - | 0.05 |
| Footer | 30KB | 20KB | 100KB | - | 0.05 |
| 3rd Party | 200KB | - | - | 300ms | 0 |
| Total | 530KB | 110KB | 1000KB | 2300ms | 0.1 |
Fix 2: Implement Automated Budget Monitoring
CI/CD Performance Checks:
Lighthouse CI Configuration:
Create
lighthouserc.json:{ "ci": { "collect": { "numberOfRuns": 3, "url": [ "http://localhost:3000/", "http://localhost:3000/products", "http://localhost:3000/checkout" ] }, "assert": { "assertions": { "categories:performance": ["error", {"minScore": 0.9}], "largest-contentful-paint": ["error", {"maxNumericValue": 2500}], "cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}], "total-blocking-time": ["error", {"maxNumericValue": 200}], "interactive": ["error", {"maxNumericValue": 3500}], "first-contentful-paint": ["error", {"maxNumericValue": 1800}], "resource-summary:script:size": ["error", {"maxNumericValue": 300000}], "resource-summary:stylesheet:size": ["error", {"maxNumericValue": 50000}], "resource-summary:image:size": ["error", {"maxNumericValue": 500000}] } }, "upload": { "target": "temporary-public-storage" } } }GitHub Actions Workflow:
name: Performance Budget CI on: [pull_request] jobs: lighthouse: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Run Lighthouse CI run: | npm install -g @lhci/cli lhci autorun - name: Upload results uses: actions/upload-artifact@v3 with: name: lighthouse-results path: .lighthouseciBundle Size Monitoring:
// bundlesize.config.js module.exports = { files: [ { path: './dist/main.js', maxSize: '300 KB' }, { path: './dist/main.css', maxSize: '50 KB' }, { path: './dist/vendor.js', maxSize: '200 KB' } ] };
Fix 3: Optimize for LCP Budget
Ensure LCP stays under 2.5 seconds:
Identify your LCP element:
new PerformanceObserver((list) => { const entries = list.getEntries(); const lastEntry = entries[entries.length - 1]; console.log('LCP element:', lastEntry.element); console.log('LCP time:', lastEntry.startTime); }).observe({type: 'largest-contentful-paint', buffered: true});Optimize LCP element loading:
<!-- If LCP is an image, preload it --> <link rel="preload" as="image" href="/hero-image.webp" fetchpriority="high"> <!-- Or use fetchpriority attribute --> <img src="/hero-image.webp" fetchpriority="high" alt="Hero">Reduce LCP element render time:
- Remove render-blocking CSS for above-fold content
- Inline critical CSS
- Defer non-critical JavaScript
- Use fast web fonts or system fonts
- Optimize images (WebP, AVIF, proper sizing)
Fix 4: Manage INP Budget
Keep interactions under 200ms:
Identify slow interactions:
new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.duration > 200) { console.warn('Slow interaction:', { name: entry.name, duration: entry.duration, target: entry.target }); } } }).observe({type: 'event', buffered: true, durationThreshold: 200});Break up long tasks:
// Bad: Long synchronous task function processLargeArray(items) { for (let i = 0; i < items.length; i++) { processItem(items[i]); } } // Good: Yield to main thread async function processLargeArray(items) { for (let i = 0; i < items.length; i++) { processItem(items[i]); // Yield every 50 items if (i % 50 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); } } }Debounce expensive operations:
function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Use for search, resize, scroll const handleSearch = debounce((query) => { performExpensiveSearch(query); }, 300);Use web workers for heavy computation:
// worker.js self.addEventListener('message', (e) => { const result = performHeavyCalculation(e.data); self.postMessage(result); }); // main.js const worker = new Worker('worker.js'); worker.postMessage(data); worker.addEventListener('message', (e) => { console.log('Result:', e.data); });
Fix 5: Control CLS Budget
Maintain CLS under 0.1:
Reserve space for dynamic content:
/* Reserve space for images */ img { aspect-ratio: 16 / 9; width: 100%; height: auto; } /* Or use explicit dimensions */ <img src="image.jpg" width="800" height="600" alt="...">Avoid inserting content above existing content:
// Bad: Inserting banner above content document.body.insertBefore(banner, document.body.firstChild); // Good: Reserve space for banner in HTML <div id="banner-placeholder" style="min-height: 100px;"> <!-- Banner loaded here --> </div>Use transform for animations:
/* Bad: Causes layout shift */ .element { transition: margin-top 0.3s; } .element:hover { margin-top: -10px; } /* Good: No layout shift */ .element { transition: transform 0.3s; } .element:hover { transform: translateY(-10px); }Load web fonts properly:
@font-face { font-family: 'CustomFont'; src: url('/fonts/custom.woff2') format('woff2'); font-display: swap; /* or optional */ } /* Fallback font with similar metrics */ body { font-family: 'CustomFont', 'Arial', sans-serif; }
Fix 6: Monitor and Alert on Budget Violations
Set up real-time monitoring:
Real User Monitoring (RUM):
import {onCLS, onINP, onLCP} from 'web-vitals'; const budgets = { LCP: 2500, INP: 200, CLS: 0.1 }; function sendVitals(metric) { // Send to analytics gtag('event', metric.name, { value: Math.round(metric.value), metric_id: metric.id, metric_delta: metric.delta, budget_status: metric.value <= budgets[metric.name] ? 'pass' : 'fail' }); // Alert if budget violated if (metric.value > budgets[metric.name]) { console.warn(`Budget violated: ${metric.name} = ${metric.value}`); // Send alert to monitoring system } } onCLS(sendVitals); onINP(sendVitals); onLCP(sendVitals);Set up alerts in Google Analytics:
- Create custom alerts for Core Web Vitals
- Set thresholds based on your budgets
- Receive email notifications when violated
Use Cloudflare Web Analytics or similar:
- Monitor Core Web Vitals across all pages
- Set up budget thresholds
- Get alerts when performance degrades
Fix 7: Create Performance Budget Culture
Make budgets part of development workflow:
Document budgets clearly:
- Include in README
- Add to style guide
- Make visible in project management tools
Review performance in PRs:
- Require Lighthouse CI to pass
- Review bundle size changes
- Discuss performance impact of new features
Regular performance audits:
- Weekly/monthly review of Core Web Vitals
- Identify pages violating budgets
- Prioritize fixes
Performance champions:
- Assign team members to monitor budgets
- Share performance wins
- Create accountability
Platform-Specific Guides
Detailed implementation instructions for your specific platform:
Verification
After implementing performance budgets:
Run Lighthouse with budgets enabled:
- Should pass all budget assertions
- No warnings about exceeded budgets
- Performance score > 90
Check CrUX data (wait 28 days):
- All Core Web Vitals in green
- 75th percentile meets thresholds
- Mobile and desktop both passing
Monitor in production:
- Real user metrics stay within budgets
- No performance regressions
- Alerts not triggering
Test budget enforcement:
- Intentionally add heavy resource
- CI/CD should fail
- Budget violation detected
Common Mistakes
- Setting unrealistic budgets - Too strict budgets that can't be met
- Ignoring real user data - Only using lab data
- Not enforcing budgets - Setting but not monitoring
- One-size-fits-all budgets - Same budget for all page types
- Forgetting third-party impact - Not budgeting for ads, analytics
- No budget for CLS - Only focusing on loading metrics
- Not updating budgets - Keeping outdated thresholds
- Missing mobile budgets - Only testing desktop
- No component-level budgets - Only page-level tracking
- Lack of team buy-in - Budgets not part of culture