Frontend Performance Budgets: Setting and Enforcing Limits
Set and enforce performance budgets with Webpack, Lighthouse CI, and bundle analyzers. Covers automated testing, CI/CD integration, and team accountability.
Introduction
Performance budgets are constraints that help teams maintain application performance over time. Without them, bundle sizes creep up, metrics degrade, and users suffer.
Establishing Performance Budgets
Types of Budgets
interface PerformanceBudget { // Quantity-based maxBundleSize: number; // Total JS bundle size maxInitialBundleSize: number; // Initial/critical JS maxImageSize: number; // Per-image limit maxTotalPageWeight: number; // All resources combined maxRequests: number; // HTTP requests per page // Time-based maxTimeToInteractive: number; // TTI in ms maxFirstContentfulPaint: number; // FCP in ms maxLargestContentfulPaint: number; // LCP in ms // Score-based minLighthousePerformance: number; // 0-100 minLighthouseAccessibility: number; minLighthouseSEO: number; } const budget: PerformanceBudget = { maxBundleSize: 250 * 1024, // 250 KB maxInitialBundleSize: 100 * 1024, // 100 KB maxImageSize: 200 * 1024, // 200 KB per image maxTotalPageWeight: 1.5 * 1024 * 1024, // 1.5 MB maxRequests: 50, maxTimeToInteractive: 3500, // 3.5 seconds maxFirstContentfulPaint: 1500, // 1.5 seconds maxLargestContentfulPaint: 2500, // 2.5 seconds minLighthousePerformance: 90, minLighthouseAccessibility: 95, minLighthouseSEO: 95, };
Bundle Analysis
Webpack Bundle Analyzer
// webpack.config.js const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin({ analyzerMode: process.env.ANALYZE ? 'server' : 'disabled', generateStatsFile: true, statsFilename: 'bundle-stats.json', }), ], };
Bundle Size Check in CI
# .github/workflows/bundle-check.yml name: Bundle Size Check on: [pull_request] jobs: bundle-size: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node uses: actions/setup-node@v3 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Check bundle size uses: preactjs/compressed-size-action@v2 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" pattern: "./dist/**/*.{js,css}" compression: "gzip"
Lighthouse CI Integration
Configuration
// lighthouserc.js module.exports = { ci: { collect: { url: ['http://localhost:3000/', 'http://localhost:3000/blog'], numberOfRuns: 3, startServerCommand: 'npm run preview', startServerReadyPattern: 'Local:', }, assert: { assertions: { 'categories:performance': ['error', { minScore: 0.9 }], 'categories:accessibility': ['error', { minScore: 0.95 }], 'categories:seo': ['error', { minScore: 0.95 }], 'categories:best-practices': ['warn', { minScore: 0.9 }], // Core Web Vitals 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }], 'first-contentful-paint': ['error', { maxNumericValue: 1500 }], 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }], 'total-blocking-time': ['error', { maxNumericValue: 300 }], // Resource budgets 'resource-summary:script:size': ['error', { maxNumericValue: 250000 }], 'resource-summary:total:size': ['error', { maxNumericValue: 1500000 }], }, }, upload: { target: 'temporary-public-storage', }, }, };
GitHub Actions Integration
# .github/workflows/lighthouse.yml name: Lighthouse CI on: [pull_request] jobs: lighthouse: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node uses: actions/setup-node@v3 with: node-version: '20' - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Run Lighthouse CI uses: treosh/lighthouse-ci-action@v10 with: configPath: './lighthouserc.js' uploadArtifacts: true temporaryPublicStorage: true
Automated Monitoring
Performance Tracking
// Track performance in production function initPerformanceMonitoring() { // Navigation Timing window.addEventListener('load', () => { setTimeout(() => { const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; sendMetrics({ dns: timing.domainLookupEnd - timing.domainLookupStart, tcp: timing.connectEnd - timing.connectStart, ttfb: timing.responseStart - timing.requestStart, download: timing.responseEnd - timing.responseStart, domInteractive: timing.domInteractive - timing.fetchStart, domComplete: timing.domComplete - timing.fetchStart, loadComplete: timing.loadEventEnd - timing.fetchStart, }); }, 0); }); // Resource Timing const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.initiatorType === 'script' || entry.initiatorType === 'css') { sendResourceMetric({ name: entry.name, type: entry.initiatorType, size: (entry as PerformanceResourceTiming).transferSize, duration: entry.duration, }); } } }); observer.observe({ type: 'resource', buffered: true }); }
Alerting on Regression
// Alert when metrics exceed budgets async function checkAndAlert(metrics: PerformanceMetrics) { const violations: string[] = []; if (metrics.bundleSize > budget.maxBundleSize) { violations.push( `Bundle size (${formatBytes(metrics.bundleSize)}) exceeds budget (${formatBytes(budget.maxBundleSize)})` ); } if (metrics.lcp > budget.maxLargestContentfulPaint) { violations.push( `LCP (${metrics.lcp}ms) exceeds budget (${budget.maxLargestContentfulPaint}ms)` ); } if (metrics.lighthousePerformance < budget.minLighthousePerformance) { violations.push( `Lighthouse Performance (${metrics.lighthousePerformance}) below minimum (${budget.minLighthousePerformance})` ); } if (violations.length > 0) { await sendSlackAlert({ channel: '#performance-alerts', text: `⚠️ Performance budget violations:\n${violations.join('\n')}`, }); } }
Optimization Strategies
Code Splitting
// Route-based splitting const routes = [ { path: '/', component: lazy(() => import('./pages/Home')), }, { path: '/blog', component: lazy(() => import('./pages/Blog')), }, { path: '/blog/:slug', component: lazy(() => import('./pages/BlogPost')), }, ]; // Component-based splitting for heavy libraries const HeavyChart = lazy(() => import('./components/HeavyChart'));
Tree Shaking
// ❌ Imports entire library import _ from 'lodash'; _.debounce(fn, 300); // ✅ Imports only what's needed import debounce from 'lodash/debounce'; debounce(fn, 300); // ✅ Or use lodash-es for better tree shaking import { debounce } from 'lodash-es';
Conclusion
Performance budgets succeed when:
- Defined clearly: Specific numbers, not vague goals
- Automated enforcement: CI/CD blocks violations
- Monitored continuously: RUM in production
- Team buy-in: Everyone understands the importance
- Regular review: Adjust budgets as needed
Performance is a feature. Budgets ensure you don't accidentally take it away from users.
Related Articles
Performance & SEO19 min read
Core Web Vitals Optimization: A Technical Deep Dive
Improve Core Web Vitals scores with practical LCP, FID, and CLS optimization techniques. Real before/after metrics from production React and Next.js applications.
Frontend Engineering17 min read
React Performance Optimization at Scale
Optimize React performance with useMemo, useCallback, and code splitting. Real performance metrics and patterns from large-scale production applications.
Performance & SEO17 min read
Technical SEO for Developers: Beyond the Basics
Master technical SEO with schema markup, meta tags, and site speed optimization. Covers canonical URLs, sitemaps, and structured data implementation.
Frontend Engineering18 min read
Micro-Frontends: Architecture and Implementation Patterns
Build micro-frontends with Webpack Module Federation and Single-SPA. Covers team organization, shared dependencies, and production deployment patterns.
Frontend Engineering15 min read
State Management Architecture in Modern React
Compare React state management solutions: Redux Toolkit, Zustand, and Context API. Learn when to use each with real examples and performance considerations.