Performance & SEO

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.

Khalid Aboubakr
16 min read
Performance BudgetBundle SizeLighthouseOptimizationMonitoringWebpackCi Cd

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:

  1. Defined clearly: Specific numbers, not vague goals
  2. Automated enforcement: CI/CD blocks violations
  3. Monitored continuously: RUM in production
  4. Team buy-in: Everyone understands the importance
  5. 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 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.