Back to blog
PerformanceJanuary 1, 2024· 13 min read

Website Performance Budget Implementation: A Developer's Guide

A practical Fusebox guide to website performance budget implementation.

Website Performance Budget Implementation: A Developer's Guide

Performance budgets are the speed limits of web development—they keep your site fast by setting hard limits on what you can add. Amazon found that every 100ms of latency cost them 1% in sales. Google discovered that 53% of mobile users abandon sites that take longer than 3 seconds to load. A performance budget ensures your site stays fast as it grows.

Why Performance Budgets Matter

In 2019, a major e-commerce site's Black Friday campaign failed when their homepage ballooned to 8MB after adding "just one more" marketing banner. Load times exceeded 15 seconds on mobile, resulting in a 60% drop in conversions. Performance budgets prevent this death by a thousand cuts—each small addition that eventually kills your site's speed.

Performance Budget Calculator

Comprehensive budget calculation and tracking:

// Performance Budget Calculator
class PerformanceBudgetCalculator {
    constructor() {
        this.budgets = {
            // Size budgets (in bytes)
            size: {
                html: 50 * 1024,        // 50KB
                css: 100 * 1024,        // 100KB
                js: 300 * 1024,         // 300KB
                images: 500 * 1024,     // 500KB per image
                fonts: 100 * 1024,      // 100KB
                total: 1500 * 1024      // 1.5MB total
            },
            // Count budgets
            count: {
                requests: 50,
                domains: 10,
                cssFiles: 3,
                jsFiles: 10,
                fonts: 5
            },
            // Timing budgets (in ms)
            timing: {
                ttfb: 600,              // Time to First Byte
                fcp: 1800,              // First Contentful Paint
                lcp: 2500,              // Largest Contentful Paint
                fid: 100,               // First Input Delay
                cls: 0.1,               // Cumulative Layout Shift
                tti: 3800,              // Time to Interactive
                totalBlockingTime: 300
            },
            // Custom metrics
            custom: {
                heroImageLoad: 1000,
                searchResults: 500,
                cartUpdate: 200
            }
        };
        
        this.measurements = {};
        this.violations = [];
    }
    
    // Measure current performance
    async measureCurrentPerformance() {
        const measurements = {
            size: await this.measureSizes(),
            count: this.measureCounts(),
            timing: await this.measureTimings(),
            custom: await this.measureCustomMetrics()
        };
        
        this.measurements = measurements;
        this.checkBudgets();
        
        return measurements;
    }
    
    // Measure resource sizes
    async measureSizes() {
        const sizes = {
            html: 0,
            css: 0,
            js: 0,
            images: 0,
            fonts: 0,
            total: 0,
            breakdown: []
        };
        
        // Get resource timing data
        const resources = performance.getEntriesByType('resource');
        const navigation = performance.getEntriesByType('navigation')[0];
        
        // Add HTML size
        if (navigation) {
            sizes.html = navigation.transferSize || 0;
            sizes.total += sizes.html;
        }
        
        // Categorize resources
        resources.forEach(resource => {
            const size = resource.transferSize || 0;
            const name = resource.name;
            
            let category = 'other';
            
            if (name.match(/\.css(\?|$)/i)) {
                category = 'css';
                sizes.css += size;
            } else if (name.match(/\.js(\?|$)/i)) {
                category = 'js';
                sizes.js += size;
            } else if (name.match(/\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i)) {
                category = 'images';
                sizes.images += size;
            } else if (name.match(/\.(woff|woff2|ttf|otf|eot)(\?|$)/i)) {
                category = 'fonts';
                sizes.fonts += size;
            }
            
            sizes.total += size;
            
            sizes.breakdown.push({
                url: name,
                category,
                size,
                compressed: resource.encodedBodySize || 0,
                duration: resource.duration
            });
        });
        
        // Sort breakdown by size
        sizes.breakdown.sort((a, b) => b.size - a.size);
        
        return sizes;
    }
    
    // Measure resource counts
    measureCounts() {
        const counts = {
            requests: 0,
            domains: new Set(),
            cssFiles: 0,
            jsFiles: 0,
            fonts: 0,
            breakdown: {}
        };
        
        const resources = performance.getEntriesByType('resource');
        counts.requests = resources.length + 1; // +1 for HTML
        
        resources.forEach(resource => {
            const url = new URL(resource.name);
            counts.domains.add(url.hostname);
            
            if (resource.name.match(/\.css(\?|$)/i)) {
                counts.cssFiles++;
            } else if (resource.name.match(/\.js(\?|$)/i)) {
                counts.jsFiles++;
            } else if (resource.name.match(/\.(woff|woff2|ttf|otf|eot)(\?|$)/i)) {
                counts.fonts++;
            }
        });
        
        counts.domains = counts.domains.size;
        
        // Group by domain
        const domainCounts = {};
        resources.forEach(resource => {
            const hostname = new URL(resource.name).hostname;
            domainCounts[hostname] = (domainCounts[hostname] || 0) + 1;
        });
        
        counts.breakdown = domainCounts;
        
        return counts;
    }
    
    // Measure performance timings
    async measureTimings() {
        const timings = {
            ttfb: 0,
            fcp: 0,
            lcp: 0,
            fid: 0,
            cls: 0,
            tti: 0,
            totalBlockingTime: 0
        };
        
        // Get navigation timing
        const navigation = performance.getEntriesByType('navigation')[0];
        if (navigation) {
            timings.ttfb = navigation.responseStart - navigation.requestStart;
        }
        
        // Get paint timing
        const paintEntries = performance.getEntriesByType('paint');
        paintEntries.forEach(entry => {
            if (entry.name === 'first-contentful-paint') {
                timings.fcp = entry.startTime;
            }
        });
        
        // Get Web Vitals
        try {
            // LCP
            const lcpEntries = performance.getEntriesByType('largest-contentful-paint');
            if (lcpEntries.length > 0) {
                timings.lcp = lcpEntries[lcpEntries.length - 1].startTime;
            }
            
            // CLS
            let clsValue = 0;
            const clsEntries = performance.getEntriesByType('layout-shift');
            clsEntries.forEach(entry => {
                if (!entry.hadRecentInput) {
                    clsValue += entry.value;
                }
            });
            timings.cls = clsValue;
            
            // Estimate TTI (simplified)
            timings.tti = timings.fcp + 2000; // Rough estimate
            
        } catch (e) {
            console.log('Some Web Vitals not available');
        }
        
        return timings;
    }
    
    // Measure custom metrics
    async measureCustomMetrics() {
        const custom = {};
        
        // Example: Measure hero image load time
        const heroImage = document.querySelector('[data-hero-image]');
        if (heroImage && heroImage.complete) {
            const imageEntry = performance.getEntriesByName(heroImage.src)[0];
            if (imageEntry) {
                custom.heroImageLoad = imageEntry.responseEnd;
            }
        }
        
        // Add your custom metrics here
        
        return custom;
    }
    
    // Check budgets against measurements
    checkBudgets() {
        this.violations = [];
        
        // Check size budgets
        Object.entries(this.budgets.size).forEach(([key, budget]) => {
            const actual = this.measurements.size[key];
            if (actual > budget) {
                this.violations.push({
                    type: 'size',
                    metric: key,
                    budget,
                    actual,
                    overBy: actual - budget,
                    percentage: ((actual - budget) / budget * 100).toFixed(2)
                });
            }
        });
        
        // Check count budgets
        Object.entries(this.budgets.count).forEach(([key, budget]) => {
            const actual = this.measurements.count[key];
            if (actual > budget) {
                this.violations.push({
                    type: 'count',
                    metric: key,
                    budget,
                    actual,
                    overBy: actual - budget,
                    percentage: ((actual - budget) / budget * 100).toFixed(2)
                });
            }
        });
        
        // Check timing budgets
        Object.entries(this.budgets.timing).forEach(([key, budget]) => {
            const actual = this.measurements.timing[key];
            if (actual > budget) {
                this.violations.push({
                    type: 'timing',
                    metric: key,
                    budget,
                    actual,
                    overBy: actual - budget,
                    percentage: ((actual - budget) / budget * 100).toFixed(2)
                });
            }
        });
        
        return this.violations;
    }
    
    // Generate report
    generateReport() {
        const report = {
            timestamp: new Date().toISOString(),
            summary: {
                totalViolations: this.violations.length,
                criticalViolations: this.violations.filter(v => v.percentage > 50).length,
                score: this.calculateScore()
            },
            measurements: this.measurements,
            violations: this.violations,
            recommendations: this.generateRecommendations()
        };
        
        return report;
    }
    
    // Calculate performance score
    calculateScore() {
        if (this.violations.length === 0) return 100;
        
        let score = 100;
        
        this.violations.forEach(violation => {
            const penalty = Math.min(20, parseFloat(violation.percentage) / 5);
            score -= penalty;
        });
        
        return Math.max(0, Math.round(score));
    }
    
    // Generate recommendations
    generateRecommendations() {
        const recommendations = [];
        
        // Size recommendations
        if (this.measurements.size.js > this.budgets.size.js) {
            recommendations.push({
                priority: 'high',
                type: 'javascript',
                issue: 'JavaScript bundle too large',
                suggestions: [
                    'Implement code splitting',
                    'Remove unused dependencies',
                    'Use dynamic imports for non-critical code',
                    'Minify and compress JavaScript files'
                ],
                potential: `Could save ${this.formatBytes(this.measurements.size.js - this.budgets.size.js)}`
            });
        }
        
        if (this.measurements.size.images > this.budgets.size.images) {
            recommendations.push({
                priority: 'high',
                type: 'images',
                issue: 'Images too large',
                suggestions: [
                    'Use modern formats (WebP, AVIF)',
                    'Implement responsive images',
                    'Lazy load below-the-fold images',
                    'Optimize image compression'
                ]
            });
        }
        
        // Timing recommendations
        if (this.measurements.timing.lcp > this.budgets.timing.lcp) {
            recommendations.push({
                priority: 'critical',
                type: 'lcp',
                issue: 'Largest Contentful Paint too slow',
                suggestions: [
                    'Optimize server response time',
                    'Preload critical resources',
                    'Reduce render-blocking resources',
                    'Optimize images and fonts'
                ]
            });
        }
        
        return recommendations;
    }
    
    // Format bytes
    formatBytes(bytes) {
        if (bytes === 0) return '0 Bytes';
        const k = 1024;
        const sizes = ['Bytes', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }
}

// Usage
const budgetCalculator = new PerformanceBudgetCalculator();
budgetCalculator.measureCurrentPerformance().then(measurements => {
    console.log('Performance Report:', budgetCalculator.generateReport());
});

Budget Enforcement System

Enforce budgets in your build process:

// Performance Budget Enforcer
class PerformanceBudgetEnforcer {
    constructor(budgetConfig) {
        this.config = budgetConfig;
        this.results = {
            passed: true,
            violations: [],
            warnings: []
        };
    }
    
    // Check bundle sizes
    async checkBundleSizes(bundles) {
        const violations = [];
        
        bundles.forEach(bundle => {
            const budget = this.config.bundles[bundle.name] || this.config.bundles.default;
            
            if (bundle.size > budget.error) {
                violations.push({
                    severity: 'error',
                    bundle: bundle.name,
                    size: bundle.size,
                    limit: budget.error,
                    message: `Bundle ${bundle.name} exceeds error threshold`
                });
                this.results.passed = false;
            } else if (bundle.size > budget.warning) {
                this.results.warnings.push({
                    severity: 'warning',
                    bundle: bundle.name,
                    size: bundle.size,
                    limit: budget.warning,
                    message: `Bundle ${bundle.name} exceeds warning threshold`
                });
            }
        });
        
        this.results.violations.push(...violations);
        return violations.length === 0;
    }
    
    // Webpack plugin for budget enforcement
    createWebpackPlugin() {
        const enforcer = this;
        
        return class PerformanceBudgetPlugin {
            apply(compiler) {
                compiler.hooks.afterEmit.tapAsync('PerformanceBudgetPlugin', (compilation, callback) => {
                    const bundles = [];
                    
                    for (const [name, asset] of compilation.assets) {
                        bundles.push({
                            name,
                            size: asset.size()
                        });
                    }
                    
                    enforcer.checkBundleSizes(bundles).then(passed => {
                        if (!passed) {
                            compilation.errors.push(new Error(
                                'Performance budget exceeded!\n' +
                                enforcer.results.violations.map(v => v.message).join('\n')
                            ));
                        }
                        
                        enforcer.results.warnings.forEach(warning => {
                            compilation.warnings.push(new Error(warning.message));
                        });
                        
                        callback();
                    });
                });
            }
        };
    }
    
    // CI/CD integration
    generateCIScript() {
        return `
#!/bin/bash
# Performance Budget Check

echo "Checking performance budgets..."

# Run Lighthouse CI
npm install -g @lhci/cli
lhci autorun --config=lighthouserc.json

# Check bundle sizes
BUNDLE_SIZES=$(node -e "
const fs = require('fs');
const path = require('path');

const distDir = './dist';
const bundles = fs.readdirSync(distDir)
    .filter(file => file.endsWith('.js'))
    .map(file => ({
        name: file,
        size: fs.statSync(path.join(distDir, file)).size
    }));

console.log(JSON.stringify(bundles));
")

# Run budget check
node -e "
const bundles = $BUNDLE_SIZES;
const budgets = ${JSON.stringify(this.config.bundles)};

let failed = false;
bundles.forEach(bundle => {
    const budget = budgets[bundle.name] || budgets.default;
    if (bundle.size > budget.error) {
        console.error(\`❌ \${bundle.name}: \${bundle.size} bytes (limit: \${budget.error})\`);
        failed = true;
    } else if (bundle.size > budget.warning) {
        console.warn(\`⚠️  \${bundle.name}: \${bundle.size} bytes (warning: \${budget.warning})\`);
    } else {
        console.log(\`✅ \${bundle.name}: \${bundle.size} bytes\`);
    }
});

if (failed) {
    process.exit(1);
}
"`;
    }
    
    // Lighthouse configuration
    generateLighthouseConfig() {
        return {
            ci: {
                collect: {
                    numberOfRuns: 3,
                    url: ['http://localhost:3000']
                },
                assert: {
                    assertions: {
                        'categories:performance': ['error', { minScore: 0.9 }],
                        'first-contentful-paint': ['error', { maxNumericValue: this.config.metrics.fcp }],
                        'largest-contentful-paint': ['error', { maxNumericValue: this.config.metrics.lcp }],
                        'cumulative-layout-shift': ['error', { maxNumericValue: this.config.metrics.cls }],
                        'total-blocking-time': ['error', { maxNumericValue: this.config.metrics.tbt }],
                        'resource-summary:script:size': ['error', { maxNumericValue: this.config.resources.js }],
                        'resource-summary:stylesheet:size': ['error', { maxNumericValue: this.config.resources.css }],
                        'resource-summary:image:size': ['error', { maxNumericValue: this.config.resources.images }],
                        'resource-summary:total:size': ['error', { maxNumericValue: this.config.resources.total }]
                    }
                },
                upload: {
                    target: 'temporary-public-storage'
                }
            }
        };
    }
}

// Example configuration
const budgetConfig = {
    bundles: {
        'main.js': { warning: 200 * 1024, error: 250 * 1024 },
        'vendor.js': { warning: 300 * 1024, error: 400 * 1024 },
        default: { warning: 100 * 1024, error: 150 * 1024 }
    },
    metrics: {
        fcp: 1800,
        lcp: 2500,
        cls: 0.1,
        tbt: 300
    },
    resources: {
        js: 300 * 1024,
        css: 100 * 1024,
        images: 500 * 1024,
        total: 1500 * 1024
    }
};

const enforcer = new PerformanceBudgetEnforcer(budgetConfig);

Real-time Budget Monitor

Monitor performance budgets in real-time:

// Real-time Performance Budget Monitor
class RealTimeBudgetMonitor {
    constructor(budgets) {
        this.budgets = budgets;
        this.metrics = {
            resources: new Map(),
            timings: {},
            violations: []
        };
        this.observers = {};
        this.callbacks = [];
    }
    
    // Start monitoring
    start() {
        this.setupResourceObserver();
        this.setupLayoutShiftObserver();
        this.setupLongTaskObserver();
        this.monitorMemory();
        
        console.log('Performance budget monitoring started');
    }
    
    // Monitor resource loading
    setupResourceObserver() {
        this.observers.resource = new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
                this.trackResource(entry);
            }
        });
        
        this.observers.resource.observe({ entryTypes: ['resource'] });
    }
    
    // Track individual resources
    trackResource(entry) {
        const size = entry.transferSize || 0;
        const type = this.getResourceType(entry.name);
        
        // Update totals
        if (!this.metrics.resources.has(type)) {
            this.metrics.resources.set(type, {
                count: 0,
                totalSize: 0,
                items: []
            });
        }
        
        const typeMetrics = this.metrics.resources.get(type);
        typeMetrics.count++;
        typeMetrics.totalSize += size;
        typeMetrics.items.push({
            url: entry.name,
            size,
            duration: entry.duration
        });
        
        // Check budget
        if (this.budgets.size[type] && typeMetrics.totalSize > this.budgets.size[type]) {
            this.addViolation({
                type: 'size',
                category: type,
                budget: this.budgets.size[type],
                actual: typeMetrics.totalSize,
                resource: entry.name
            });
        }
    }
    
    // Monitor layout shifts
    setupLayoutShiftObserver() {
        let clsValue = 0;
        
        this.observers.layoutShift = new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
                if (!entry.hadRecentInput) {
                    clsValue += entry.value;
                    
                    if (clsValue > this.budgets.timing.cls) {
                        this.addViolation({
                            type: 'timing',
                            metric: 'cls',
                            budget: this.budgets.timing.cls,
                            actual: clsValue
                        });
                    }
                }
            }
        });
        
        try {
            this.observers.layoutShift.observe({ entryTypes: ['layout-shift'] });
        } catch (e) {
            console.log('Layout shift observer not supported');
        }
    }
    
    // Monitor long tasks
    setupLongTaskObserver() {
        let totalBlockingTime = 0;
        
        this.observers.longTask = new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
                const blockingTime = entry.duration - 50; // Tasks over 50ms are blocking
                if (blockingTime > 0) {
                    totalBlockingTime += blockingTime;
                    
                    if (totalBlockingTime > this.budgets.timing.totalBlockingTime) {
                        this.addViolation({
                            type: 'timing',
                            metric: 'totalBlockingTime',
                            budget: this.budgets.timing.totalBlockingTime,
                            actual: totalBlockingTime
                        });
                    }
                }
            }
        });
        
        try {
            this.observers.longTask.observe({ entryTypes: ['longtask'] });
        } catch (e) {
            console.log('Long task observer not supported');
        }
    }
    
    // Monitor memory usage
    monitorMemory() {
        if (!performance.memory) return;
        
        setInterval(() => {
            const memoryMB = performance.memory.usedJSHeapSize / (1024 * 1024);
            
            if (this.budgets.memory && memoryMB > this.budgets.memory) {
                this.addViolation({
                    type: 'memory',
                    budget: this.budgets.memory,
                    actual: memoryMB
                });
            }
        }, 5000);
    }
    
    // Add violation
    addViolation(violation) {
        violation.timestamp = new Date().toISOString();
        this.metrics.violations.push(violation);
        
        // Notify callbacks
        this.callbacks.forEach(callback => {
            callback('violation', violation);
        });
        
        console.warn('Performance budget violation:', violation);
    }
    
    // Get resource type
    getResourceType(url) {
        if (url.match(/\.css(\?|$)/i)) return 'css';
        if (url.match(/\.js(\?|$)/i)) return 'js';
        if (url.match(/\.(jpg|jpeg|png|gif|webp|svg)(\?|$)/i)) return 'images';
        if (url.match(/\.(woff|woff2|ttf|otf|eot)(\?|$)/i)) return 'fonts';
        return 'other';
    }
    
    // Subscribe to events
    subscribe(callback) {
        this.callbacks.push(callback);
    }
    
    // Get current status
    getStatus() {
        const status = {
            resources: {},
            violations: this.metrics.violations,
            score: 100
        };
        
        // Convert resource map to object
        for (const [type, data] of this.metrics.resources) {
            status.resources[type] = {
                count: data.count,
                totalSize: data.totalSize,
                budget: this.budgets.size[type] || null,
                withinBudget: !this.budgets.size[type] || data.totalSize <= this.budgets.size[type]
            };
        }
        
        // Calculate score
        status.score = Math.max(0, 100 - (this.metrics.violations.length * 10));
        
        return status;
    }
    
    // Stop monitoring
    stop() {
        Object.values(this.observers).forEach(observer => observer.disconnect());
        console.log('Performance budget monitoring stopped');
    }
}

// Usage
const monitor = new RealTimeBudgetMonitor({
    size: {
        js: 300 * 1024,
        css: 100 * 1024,
        images: 500 * 1024,
        fonts: 100 * 1024
    },
    timing: {
        cls: 0.1,
        totalBlockingTime: 300
    },
    memory: 50 // MB
});

monitor.subscribe((event, data) => {
    console.log(`Budget ${event}:`, data);
});

monitor.start();

Budget Visualization Dashboard

Create a visual dashboard for performance budgets:

// Performance Budget Dashboard
class PerformanceBudgetDashboard {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
        this.data = {
            budgets: {},
            current: {},
            history: []
        };
    }
    
    // Initialize dashboard
    init(budgets) {
        this.data.budgets = budgets;
        this.createDashboard();
        this.startMonitoring();
    }
    
    // Create dashboard HTML
    createDashboard() {
        this.container.innerHTML = `
            <div class="perf-dashboard">
                <h2>Performance Budget Dashboard</h2>
                
                <div class="perf-summary">
                    <div class="perf-score">
                        <h3>Score</h3>
                        <div class="score-value">100</div>
                    </div>
                    <div class="perf-violations">
                        <h3>Violations</h3>
                        <div class="violations-count">0</div>
                    </div>
                </div>
                
                <div class="perf-metrics">
                    <h3>Resource Budgets</h3>
                    <div id="resource-meters"></div>
                </div>
                
                <div class="perf-timing">
                    <h3>Timing Budgets</h3>
                    <div id="timing-meters"></div>
                </div>
                
                <div class="perf-history">
                    <h3>History</h3>
                    <canvas id="history-chart"></canvas>
                </div>
            </div>
            
            <style>
                .perf-dashboard {
                    font-family: system-ui, -apple-system, sans-serif;
                    padding: 20px;
                    background: #f5f5f5;
                    border-radius: 8px;
                }
                
                .perf-summary {
                    display: flex;
                    gap: 20px;
                    margin-bottom: 30px;
                }
                
                .perf-score, .perf-violations {
                    background: white;
                    padding: 20px;
                    border-radius: 8px;
                    text-align: center;
                    flex: 1;
                }
                
                .score-value {
                    font-size: 48px;
                    font-weight: bold;
                    color: #4CAF50;
                }
                
                .violations-count {
                    font-size: 48px;
                    font-weight: bold;
                    color: #f44336;
                }
                
                .perf-meter {
                    margin: 10px 0;
                    background: white;
                    padding: 15px;
                    border-radius: 8px;
                }
                
                .meter-label {
                    display: flex;
                    justify-content: space-between;
                    margin-bottom: 5px;
                }
                
                .meter-bar {
                    height: 20px;
                    background: #e0e0e0;
                    border-radius: 10px;
                    overflow: hidden;
                    position: relative;
                }
                
                .meter-fill {
                    height: 100%;
                    background: #4CAF50;
                    transition: width 0.3s ease;
                }
                
                .meter-fill.warning {
                    background: #ff9800;
                }
                
                .meter-fill.error {
                    background: #f44336;
                }
                
                .meter-limit {
                    position: absolute;
                    top: 0;
                    bottom: 0;
                    width: 2px;
                    background: #333;
                }
            </style>
        `;
    }
    
    // Create meter element
    createMeter(label, current, budget, unit = 'KB') {
        const percentage = (current / budget) * 100;
        const status = percentage > 100 ? 'error' : percentage > 80 ? 'warning' : '';
        
        return `
            <div class="perf-meter">
                <div class="meter-label">
                    <span>${label}</span>
                    <span>${this.formatValue(current, unit)} / ${this.formatValue(budget, unit)}</span>
                </div>
                <div class="meter-bar">
                    <div class="meter-fill ${status}" style="width: ${Math.min(percentage, 100)}%"></div>
                    <div class="meter-limit" style="left: 100%"></div>
                </div>
            </div>
        `;
    }
    
    // Format values
    formatValue(value, unit) {
        if (unit === 'KB') {
            return (value / 1024).toFixed(1) + ' KB';
        } else if (unit === 'ms') {
            return value.toFixed(0) + ' ms';
        }
        return value.toFixed(2) + ' ' + unit;
    }
    
    // Update dashboard
    updateDashboard(data) {
        // Update score
        const scoreElement = this.container.querySelector('.score-value');
        scoreElement.textContent = data.score;
        scoreElement.style.color = data.score >= 80 ? '#4CAF50' : 
                                   data.score >= 60 ? '#ff9800' : '#f44336';
        
        // Update violations
        this.container.querySelector('.violations-count').textContent = data.violations;
        
        // Update resource meters
        const resourceMeters = this.container.querySelector('#resource-meters');
        resourceMeters.innerHTML = Object.entries(data.resources).map(([type, values]) => 
            this.createMeter(
                type.charAt(0).toUpperCase() + type.slice(1),
                values.current,
                values.budget,
                'KB'
            )
        ).join('');
        
        // Update timing meters
        const timingMeters = this.container.querySelector('#timing-meters');
        timingMeters.innerHTML = Object.entries(data.timing).map(([metric, values]) => 
            this.createMeter(
                metric.toUpperCase(),
                values.current,
                values.budget,
                metric === 'cls' ? '' : 'ms'
            )
        ).join('');
        
        // Update history
        this.updateHistory(data);
    }
    
    // Update history chart
    updateHistory(data) {
        this.data.history.push({
            timestamp: Date.now(),
            score: data.score
        });
        
        // Keep last 50 data points
        if (this.data.history.length > 50) {
            this.data.history.shift();
        }
        
        // Draw chart (simplified)
        const canvas = this.container.querySelector('#history-chart');
        const ctx = canvas.getContext('2d');
        
        canvas.width = canvas.offsetWidth;
        canvas.height = 150;
        
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        // Draw grid
        ctx.strokeStyle = '#e0e0e0';
        ctx.beginPath();
        for (let i = 0; i <= 10; i++) {
            const y = (i / 10) * canvas.height;
            ctx.moveTo(0, y);
            ctx.lineTo(canvas.width, y);
        }
        ctx.stroke();
        
        // Draw line
        ctx.strokeStyle = '#4CAF50';
        ctx.lineWidth = 2;
        ctx.beginPath();
        
        this.data.history.forEach((point, index) => {
            const x = (index / (this.data.history.length - 1)) * canvas.width;
            const y = canvas.height - (point.score / 100) * canvas.height;
            
            if (index === 0) {
                ctx.moveTo(x, y);
            } else {
                ctx.lineTo(x, y);
            }
        });
        
        ctx.stroke();
    }
    
    // Start monitoring
    startMonitoring() {
        // Simulate data updates
        setInterval(() => {
            const mockData = {
                score: Math.max(0, 100 - Math.random() * 30),
                violations: Math.floor(Math.random() * 5),
                resources: {
                    js: {
                        current: 250 * 1024 + Math.random() * 100 * 1024,
                        budget: 300 * 1024
                    },
                    css: {
                        current: 80 * 1024 + Math.random() * 40 * 1024,
                        budget: 100 * 1024
                    },
                    images: {
                        current: 400 * 1024 + Math.random() * 200 * 1024,
                        budget: 500 * 1024
                    }
                },
                timing: {
                    lcp: {
                        current: 2000 + Math.random() * 1000,
                        budget: 2500
                    },
                    fid: {
                        current: 50 + Math.random() * 100,
                        budget: 100
                    },
                    cls: {
                        current: Math.random() * 0.2,
                        budget: 0.1
                    }
                }
            };
            
            this.updateDashboard(mockData);
        }, 2000);
    }
}

// Usage
// const dashboard = new PerformanceBudgetDashboard('dashboard-container');
// dashboard.init(budgetConfig);

Best Practices for Performance Budgets

  1. Start Conservative: Begin with achievable budgets and tighten over time
  2. Measure Real Users: Use RUM data to set realistic targets
  3. Budget by Journey: Different budgets for different user paths
  4. Include All Resources: Don't forget third-party scripts
  5. Automate Enforcement: Integrate into CI/CD pipeline
  6. Monitor Continuously: Track budget violations in production
  7. Communicate Clearly: Make budgets visible to all stakeholders
  8. Review Regularly: Adjust budgets based on user needs
  9. Consider Context: Mobile vs desktop, slow vs fast networks
  10. Focus on Impact: Prioritize metrics that affect users

Common Budget Violations

  • JavaScript Bloat: Unnecessary dependencies and polyfills
  • Unoptimized Images: Large images without compression
  • Third-Party Scripts: Analytics and advertising overhead
  • Web Fonts: Loading too many font variations
  • CSS Complexity: Unused styles and large frameworks
  • Lazy Loading Failures: Loading everything upfront
  • Memory Leaks: Growing memory usage over time
  • API Payload Size: Sending too much data
  • Render Blocking: Scripts and styles blocking paint
  • Cache Misses: Resources not properly cached

Remember: Performance budgets are not about limiting creativity—they're about ensuring your creative vision loads fast enough for users to experience it. Set budgets early, enforce them automatically, and treat performance as a feature, not an afterthought.