Back to blog
PerformanceJanuary 1, 2024· 13 min read

JavaScript Performance Profiling and Memory Leaks: A Developer's Guide

A practical Fusebox guide to javascript performance profiling and memory leaks.

JavaScript Performance Profiling and Memory Leaks: A Developer's Guide

JavaScript performance issues and memory leaks are silent killers of web applications. They degrade user experience gradually, causing frustration that's hard to diagnose. A study by Google found that 53% of mobile users abandon sites that take longer than 3 seconds to load, and memory leaks can turn a fast app into a sluggish one over time.

Why JavaScript Performance Matters

In 2020, a major social media platform discovered a memory leak that caused their web app to consume over 2GB of RAM after just 30 minutes of use. The fix improved user engagement by 27% and reduced bounce rates by 35%. Understanding performance profiling and memory management isn't optional—it's essential for modern web development.

Performance Profiling Basics

Start with this comprehensive performance profiler:

// Performance profiler with detailed metrics
const performanceProfiler = {
    marks: new Map(),
    measures: [],
    observers: [],
    
    // Start a performance mark
    startMark(name) {
        this.marks.set(name, {
            start: performance.now(),
            memory: performance.memory ? {
                usedJSHeapSize: performance.memory.usedJSHeapSize,
                totalJSHeapSize: performance.memory.totalJSHeapSize
            } : null
        });
    },
    
    // End a mark and create a measure
    endMark(name, detail = {}) {
        const mark = this.marks.get(name);
        if (!mark) {
            console.warn(`No start mark found for ${name}`);
            return;
        }
        
        const duration = performance.now() - mark.start;
        const memoryDelta = performance.memory ? {
            heapDelta: performance.memory.usedJSHeapSize - mark.memory.usedJSHeapSize
        } : null;
        
        const measure = {
            name,
            duration,
            memoryDelta,
            detail,
            timestamp: new Date().toISOString()
        };
        
        this.measures.push(measure);
        this.marks.delete(name);
        
        // Use Performance API
        performance.mark(`${name}-end`);
        performance.measure(name, `${name}-start`, `${name}-end`);
        
        return measure;
    },
    
    // Profile a function
    profile(fn, name = fn.name) {
        return (...args) => {
            this.startMark(name);
            try {
                const result = fn(...args);
                // Handle promises
                if (result && typeof result.then === 'function') {
                    return result.finally(() => this.endMark(name));
                }
                this.endMark(name);
                return result;
            } catch (error) {
                this.endMark(name, { error: error.message });
                throw error;
            }
        };
    },
    
    // Get performance report
    getReport() {
        const report = {
            measures: this.measures,
            summary: {
                totalDuration: this.measures.reduce((sum, m) => sum + m.duration, 0),
                avgDuration: this.measures.length ? 
                    this.measures.reduce((sum, m) => sum + m.duration, 0) / this.measures.length : 0,
                slowestOperation: this.measures.sort((a, b) => b.duration - a.duration)[0],
                memoryImpact: this.measures.reduce((sum, m) => 
                    sum + (m.memoryDelta?.heapDelta || 0), 0)
            }
        };
        
        // Group by operation name
        report.byOperation = this.measures.reduce((acc, measure) => {
            if (!acc[measure.name]) {
                acc[measure.name] = {
                    count: 0,
                    totalDuration: 0,
                    avgDuration: 0,
                    maxDuration: 0
                };
            }
            acc[measure.name].count++;
            acc[measure.name].totalDuration += measure.duration;
            acc[measure.name].maxDuration = Math.max(
                acc[measure.name].maxDuration, 
                measure.duration
            );
            return acc;
        }, {});
        
        // Calculate averages
        Object.values(report.byOperation).forEach(op => {
            op.avgDuration = op.totalDuration / op.count;
        });
        
        return report;
    }
};

// Example usage
const profiledFetch = performanceProfiler.profile(fetch, 'network-request');

Memory Leak Detection

Comprehensive memory leak detector:

// Memory leak detector
class MemoryLeakDetector {
    constructor() {
        this.snapshots = [];
        this.leaks = [];
        this.interval = null;
    }
    
    // Take a memory snapshot
    takeSnapshot(label = '') {
        if (!performance.memory) {
            console.warn('Memory API not available. Run Chrome with --enable-precise-memory-info');
            return null;
        }
        
        // Force garbage collection if available (Chrome with --js-flags="--expose-gc")
        if (window.gc) {
            window.gc();
        }
        
        const snapshot = {
            timestamp: Date.now(),
            label,
            memory: {
                usedJSHeapSize: performance.memory.usedJSHeapSize,
                totalJSHeapSize: performance.memory.totalJSHeapSize,
                jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
            },
            // Count DOM nodes
            domNodes: document.querySelectorAll('*').length,
            // Count event listeners (approximate)
            listeners: this.countEventListeners()
        };
        
        this.snapshots.push(snapshot);
        return snapshot;
    }
    
    // Count event listeners (approximate)
    countEventListeners() {
        let count = 0;
        const allElements = document.querySelectorAll('*');
        
        // Common event types to check
        const eventTypes = ['click', 'change', 'input', 'keydown', 'keyup', 
                          'mousedown', 'mouseup', 'scroll', 'resize'];
        
        allElements.forEach(element => {
            eventTypes.forEach(eventType => {
                // This is an approximation - actual listeners are not enumerable
                if (element[`on${eventType}`]) count++;
            });
        });
        
        return count;
    }
    
    // Analyze snapshots for leaks
    analyzeLeaks() {
        if (this.snapshots.length < 2) {
            return { error: 'Need at least 2 snapshots for analysis' };
        }
        
        const analysis = {
            memoryGrowth: [],
            domGrowth: [],
            listenerGrowth: [],
            potentialLeaks: []
        };
        
        // Compare consecutive snapshots
        for (let i = 1; i < this.snapshots.length; i++) {
            const prev = this.snapshots[i - 1];
            const curr = this.snapshots[i];
            
            const memoryDelta = curr.memory.usedJSHeapSize - prev.memory.usedJSHeapSize;
            const domDelta = curr.domNodes - prev.domNodes;
            const listenerDelta = curr.listeners - prev.listeners;
            
            analysis.memoryGrowth.push({
                from: prev.label,
                to: curr.label,
                delta: memoryDelta,
                percentage: ((memoryDelta / prev.memory.usedJSHeapSize) * 100).toFixed(2)
            });
            
            if (domDelta > 0) {
                analysis.domGrowth.push({
                    from: prev.label,
                    to: curr.label,
                    delta: domDelta
                });
            }
            
            if (listenerDelta > 0) {
                analysis.listenerGrowth.push({
                    from: prev.label,
                    to: curr.label,
                    delta: listenerDelta
                });
            }
        }
        
        // Detect potential leaks
        const avgGrowth = analysis.memoryGrowth.reduce((sum, g) => 
            sum + g.delta, 0) / analysis.memoryGrowth.length;
        
        if (avgGrowth > 1048576) { // 1MB average growth
            analysis.potentialLeaks.push({
                type: 'Memory',
                severity: 'High',
                description: `Average memory growth of ${(avgGrowth / 1048576).toFixed(2)}MB between snapshots`
            });
        }
        
        if (analysis.domGrowth.length > 0) {
            const totalDomGrowth = analysis.domGrowth.reduce((sum, g) => sum + g.delta, 0);
            if (totalDomGrowth > 100) {
                analysis.potentialLeaks.push({
                    type: 'DOM Nodes',
                    severity: 'Medium',
                    description: `${totalDomGrowth} DOM nodes added but not removed`
                });
            }
        }
        
        if (analysis.listenerGrowth.length > 0) {
            const totalListenerGrowth = analysis.listenerGrowth.reduce((sum, g) => sum + g.delta, 0);
            if (totalListenerGrowth > 10) {
                analysis.potentialLeaks.push({
                    type: 'Event Listeners',
                    severity: 'Medium',
                    description: `${totalListenerGrowth} event listeners added but not removed`
                });
            }
        }
        
        return analysis;
    }
    
    // Monitor for leaks over time
    startMonitoring(interval = 10000) {
        this.takeSnapshot('Initial');
        
        this.interval = setInterval(() => {
            this.takeSnapshot(`After ${(Date.now() - this.snapshots[0].timestamp) / 1000}s`);
            
            if (this.snapshots.length > 10) {
                this.snapshots.shift(); // Keep only last 10 snapshots
            }
            
            const analysis = this.analyzeLeaks();
            if (analysis.potentialLeaks && analysis.potentialLeaks.length > 0) {
                console.warn('Potential memory leaks detected:', analysis.potentialLeaks);
            }
        }, interval);
        
        console.log(`Memory monitoring started (every ${interval}ms)`);
    }
    
    stopMonitoring() {
        if (this.interval) {
            clearInterval(this.interval);
            this.interval = null;
            console.log('Memory monitoring stopped');
        }
    }
}

// Usage
const leakDetector = new MemoryLeakDetector();
leakDetector.startMonitoring();

Common Memory Leak Patterns

Detect and fix common leak patterns:

// Common memory leak patterns detector
const leakPatterns = {
    // Detect detached DOM nodes
    findDetachedNodes() {
        const detector = {
            nodes: new WeakSet(),
            detached: []
        };
        
        // Track all nodes
        document.querySelectorAll('*').forEach(node => {
            detector.nodes.add(node);
        });
        
        // Check after DOM manipulation
        return {
            check: () => {
                const allNodes = document.querySelectorAll('*');
                const currentNodes = new Set(allNodes);
                
                // Find nodes that were tracked but are no longer in DOM
                // Note: WeakSet doesn't allow iteration, so this is conceptual
                // In practice, you'd need to maintain a regular Set for comparison
                
                return detector;
            }
        };
    },
    
    // Detect circular references
    findCircularReferences(obj, seen = new WeakSet(), path = []) {
        if (obj === null || typeof obj !== 'object') return [];
        
        if (seen.has(obj)) {
            return [{
                type: 'circular-reference',
                path: path.join(' -> '),
                object: obj
            }];
        }
        
        seen.add(obj);
        const circulars = [];
        
        for (const key in obj) {
            if (obj.hasOwnProperty(key)) {
                const newPath = [...path, key];
                const found = this.findCircularReferences(obj[key], seen, newPath);
                circulars.push(...found);
            }
        }
        
        seen.delete(obj);
        return circulars;
    },
    
    // Detect timer leaks
    detectTimerLeaks() {
        const originalSetTimeout = window.setTimeout;
        const originalSetInterval = window.setInterval;
        const timers = new Map();
        
        // Override setTimeout
        window.setTimeout = function(fn, delay, ...args) {
            const id = originalSetTimeout.call(window, fn, delay, ...args);
            timers.set(id, {
                type: 'timeout',
                created: new Date(),
                stack: new Error().stack
            });
            return id;
        };
        
        // Override setInterval
        window.setInterval = function(fn, delay, ...args) {
            const id = originalSetInterval.call(window, fn, delay, ...args);
            timers.set(id, {
                type: 'interval',
                created: new Date(),
                stack: new Error().stack
            });
            return id;
        };
        
        // Override clear functions
        const originalClearTimeout = window.clearTimeout;
        window.clearTimeout = function(id) {
            timers.delete(id);
            return originalClearTimeout.call(window, id);
        };
        
        const originalClearInterval = window.clearInterval;
        window.clearInterval = function(id) {
            timers.delete(id);
            return originalClearInterval.call(window, id);
        };
        
        return {
            getActiveTimers: () => Array.from(timers.entries()),
            getLeakedTimers: () => {
                const now = new Date();
                return Array.from(timers.entries()).filter(([id, timer]) => {
                    const age = now - timer.created;
                    // Consider timers older than 5 minutes as potential leaks
                    return age > 300000;
                });
            }
        };
    },
    
    // Detect event listener leaks
    detectEventListenerLeaks() {
        const listeners = new Map();
        const originalAdd = EventTarget.prototype.addEventListener;
        const originalRemove = EventTarget.prototype.removeEventListener;
        
        EventTarget.prototype.addEventListener = function(type, listener, options) {
            const key = `${this.constructor.name}_${type}`;
            if (!listeners.has(key)) {
                listeners.set(key, new Set());
            }
            listeners.get(key).add({
                target: this,
                type,
                listener,
                addedAt: new Date()
            });
            
            return originalAdd.call(this, type, listener, options);
        };
        
        EventTarget.prototype.removeEventListener = function(type, listener, options) {
            const key = `${this.constructor.name}_${type}`;
            if (listeners.has(key)) {
                const set = listeners.get(key);
                set.forEach(item => {
                    if (item.listener === listener) {
                        set.delete(item);
                    }
                });
            }
            
            return originalRemove.call(this, type, listener, options);
        };
        
        return {
            getListenerCount: () => {
                let total = 0;
                listeners.forEach(set => total += set.size);
                return total;
            },
            getListenersByType: () => {
                const byType = {};
                listeners.forEach((set, key) => {
                    byType[key] = set.size;
                });
                return byType;
            },
            findLeakedListeners: () => {
                const leaks = [];
                const now = new Date();
                
                listeners.forEach((set, key) => {
                    set.forEach(item => {
                        // Check if element is still in DOM
                        if (item.target instanceof HTMLElement && 
                            !document.contains(item.target)) {
                            leaks.push({
                                type: 'detached-listener',
                                key,
                                age: now - item.addedAt
                            });
                        }
                    });
                });
                
                return leaks;
            }
        };
    }
};

JavaScript Execution Profiling

Profile JavaScript execution in detail:

// Execution profiler with call stack analysis
class ExecutionProfiler {
    constructor() {
        this.profiles = new Map();
        this.callStack = [];
        this.enabled = false;
    }
    
    // Wrap a function for profiling
    wrap(obj, methodName) {
        const original = obj[methodName];
        const profiler = this;
        
        obj[methodName] = function(...args) {
            if (!profiler.enabled) {
                return original.apply(this, args);
            }
            
            const start = performance.now();
            const callInfo = {
                method: methodName,
                args: args.length,
                startTime: start,
                stack: profiler.callStack.slice()
            };
            
            profiler.callStack.push(methodName);
            
            try {
                const result = original.apply(this, args);
                
                // Handle async functions
                if (result && typeof result.then === 'function') {
                    return result.finally(() => {
                        profiler.recordCall(callInfo, performance.now() - start);
                        profiler.callStack.pop();
                    });
                }
                
                profiler.recordCall(callInfo, performance.now() - start);
                profiler.callStack.pop();
                return result;
            } catch (error) {
                profiler.recordCall(callInfo, performance.now() - start, error);
                profiler.callStack.pop();
                throw error;
            }
        };
        
        // Maintain function properties
        obj[methodName]._original = original;
        obj[methodName]._profiled = true;
    }
    
    // Record call information
    recordCall(callInfo, duration, error = null) {
        const key = callInfo.stack.concat(callInfo.method).join(' -> ');
        
        if (!this.profiles.has(key)) {
            this.profiles.set(key, {
                path: key,
                calls: 0,
                totalTime: 0,
                minTime: Infinity,
                maxTime: 0,
                avgTime: 0,
                errors: 0
            });
        }
        
        const profile = this.profiles.get(key);
        profile.calls++;
        profile.totalTime += duration;
        profile.minTime = Math.min(profile.minTime, duration);
        profile.maxTime = Math.max(profile.maxTime, duration);
        profile.avgTime = profile.totalTime / profile.calls;
        
        if (error) {
            profile.errors++;
        }
    }
    
    // Start profiling
    start() {
        this.enabled = true;
        this.profiles.clear();
        this.callStack = [];
        console.log('Execution profiling started');
    }
    
    // Stop profiling
    stop() {
        this.enabled = false;
        console.log('Execution profiling stopped');
    }
    
    // Get profiling report
    getReport() {
        const profiles = Array.from(this.profiles.values());
        
        // Sort by total time
        profiles.sort((a, b) => b.totalTime - a.totalTime);
        
        const report = {
            totalCalls: profiles.reduce((sum, p) => sum + p.calls, 0),
            totalTime: profiles.reduce((sum, p) => sum + p.totalTime, 0),
            hotPaths: profiles.slice(0, 10),
            callTree: this.buildCallTree(profiles)
        };
        
        return report;
    }
    
    // Build call tree from profiles
    buildCallTree(profiles) {
        const tree = { name: 'root', children: {}, time: 0 };
        
        profiles.forEach(profile => {
            const parts = profile.path.split(' -> ');
            let current = tree;
            
            parts.forEach(part => {
                if (!current.children[part]) {
                    current.children[part] = {
                        name: part,
                        children: {},
                        time: 0,
                        calls: 0
                    };
                }
                current = current.children[part];
            });
            
            current.time += profile.totalTime;
            current.calls += profile.calls;
        });
        
        return tree;
    }
}

// Usage example
const profiler = new ExecutionProfiler();
profiler.wrap(Array.prototype, 'map');
profiler.wrap(Array.prototype, 'filter');
profiler.wrap(Array.prototype, 'reduce');
profiler.start();

Real-time Performance Monitor

Monitor performance metrics in real-time:

// Real-time performance monitor
class PerformanceMonitor {
    constructor() {
        this.metrics = {
            fps: [],
            memory: [],
            longTasks: [],
            interactions: []
        };
        this.observers = {};
        this.rafId = null;
        this.lastFrameTime = 0;
    }
    
    // Start monitoring
    start() {
        // Monitor FPS
        this.measureFPS();
        
        // Monitor long tasks
        if ('PerformanceObserver' in window) {
            try {
                this.observers.longTasks = new PerformanceObserver((list) => {
                    for (const entry of list.getEntries()) {
                        this.metrics.longTasks.push({
                            duration: entry.duration,
                            startTime: entry.startTime,
                            name: entry.name
                        });
                    }
                });
                this.observers.longTasks.observe({ entryTypes: ['longtask'] });
            } catch (e) {
                console.log('Long task monitoring not supported');
            }
        }
        
        // Monitor user interactions
        this.monitorInteractions();
        
        // Monitor memory
        if (performance.memory) {
            this.memoryInterval = setInterval(() => {
                this.metrics.memory.push({
                    timestamp: Date.now(),
                    used: performance.memory.usedJSHeapSize,
                    total: performance.memory.totalJSHeapSize,
                    limit: performance.memory.jsHeapSizeLimit
                });
                
                // Keep only last 100 samples
                if (this.metrics.memory.length > 100) {
                    this.metrics.memory.shift();
                }
            }, 1000);
        }
        
        console.log('Performance monitoring started');
    }
    
    // Measure FPS
    measureFPS() {
        const now = performance.now();
        if (this.lastFrameTime) {
            const delta = now - this.lastFrameTime;
            const fps = 1000 / delta;
            
            this.metrics.fps.push({
                timestamp: now,
                fps: Math.round(fps)
            });
            
            // Keep only last 60 samples
            if (this.metrics.fps.length > 60) {
                this.metrics.fps.shift();
            }
        }
        
        this.lastFrameTime = now;
        this.rafId = requestAnimationFrame(() => this.measureFPS());
    }
    
    // Monitor user interactions
    monitorInteractions() {
        const interactionEvents = ['click', 'keydown', 'scroll', 'touchstart'];
        
        interactionEvents.forEach(eventType => {
            document.addEventListener(eventType, (event) => {
                const start = performance.now();
                
                // Wait for next frame to measure response time
                requestAnimationFrame(() => {
                    const responseTime = performance.now() - start;
                    
                    this.metrics.interactions.push({
                        type: eventType,
                        responseTime,
                        timestamp: start,
                        target: event.target.tagName
                    });
                    
                    // Keep only last 50 interactions
                    if (this.metrics.interactions.length > 50) {
                        this.metrics.interactions.shift();
                    }
                });
            }, { passive: true });
        });
    }
    
    // Get current metrics
    getMetrics() {
        const now = performance.now();
        
        // Calculate averages
        const avgFPS = this.metrics.fps.length ? 
            this.metrics.fps.reduce((sum, m) => sum + m.fps, 0) / this.metrics.fps.length : 0;
        
        const avgResponseTime = this.metrics.interactions.length ?
            this.metrics.interactions.reduce((sum, m) => sum + m.responseTime, 0) / 
            this.metrics.interactions.length : 0;
        
        const currentMemory = this.metrics.memory[this.metrics.memory.length - 1];
        
        return {
            fps: {
                current: this.metrics.fps[this.metrics.fps.length - 1]?.fps || 0,
                average: Math.round(avgFPS),
                min: Math.min(...this.metrics.fps.map(m => m.fps)),
                dropouts: this.metrics.fps.filter(m => m.fps < 30).length
            },
            memory: {
                current: currentMemory ? 
                    (currentMemory.used / 1048576).toFixed(2) + 'MB' : 'N/A',
                trend: this.calculateMemoryTrend()
            },
            longTasks: {
                count: this.metrics.longTasks.length,
                totalDuration: this.metrics.longTasks.reduce((sum, t) => sum + t.duration, 0),
                worst: Math.max(...this.metrics.longTasks.map(t => t.duration), 0)
            },
            interactions: {
                avgResponseTime: avgResponseTime.toFixed(2) + 'ms',
                slow: this.metrics.interactions.filter(i => i.responseTime > 100).length
            }
        };
    }
    
    // Calculate memory trend
    calculateMemoryTrend() {
        if (this.metrics.memory.length < 2) return 'stable';
        
        const recent = this.metrics.memory.slice(-10);
        const first = recent[0].used;
        const last = recent[recent.length - 1].used;
        const change = ((last - first) / first) * 100;
        
        if (change > 10) return 'increasing';
        if (change < -10) return 'decreasing';
        return 'stable';
    }
    
    // Stop monitoring
    stop() {
        if (this.rafId) {
            cancelAnimationFrame(this.rafId);
        }
        
        if (this.memoryInterval) {
            clearInterval(this.memoryInterval);
        }
        
        Object.values(this.observers).forEach(observer => observer.disconnect());
        
        console.log('Performance monitoring stopped');
    }
}

// Usage
const monitor = new PerformanceMonitor();
monitor.start();

// Check metrics periodically
setInterval(() => {
    console.log(monitor.getMetrics());
}, 5000);

Optimization Recommendations Engine

Generate specific optimization recommendations:

// Performance optimization advisor
function analyzePerformanceAndAdvise() {
    const analysis = {
        issues: [],
        recommendations: [],
        score: 100
    };
    
    // Check for common performance issues
    
    // 1. Check for too many DOM nodes
    const domNodes = document.querySelectorAll('*').length;
    if (domNodes > 1500) {
        analysis.issues.push({
            type: 'DOM Size',
            severity: 'Medium',
            impact: -10,
            details: `Page has ${domNodes} DOM nodes (recommended: <1500)`
        });
        analysis.recommendations.push('Consider lazy loading or virtualization for large lists');
    }
    
    // 2. Check for layout thrashing
    const checkLayoutThrashing = () => {
        let reads = 0, writes = 0;
        const properties = ['offsetWidth', 'offsetHeight', 'scrollTop', 'clientWidth'];
        
        // Monitor property access (simplified check)
        properties.forEach(prop => {
            const descriptor = Object.getOwnPropertyDescriptor(
                HTMLElement.prototype, 
                prop
            );
            
            if (descriptor && descriptor.get) {
                reads++;
            }
        });
        
        if (reads > 10) {
            analysis.issues.push({
                type: 'Layout Thrashing',
                severity: 'High',
                impact: -15,
                details: 'Potential layout thrashing detected'
            });
            analysis.recommendations.push('Batch DOM reads and writes separately');
        }
    };
    
    // 3. Check for memory usage
    if (performance.memory) {
        const memoryMB = performance.memory.usedJSHeapSize / 1048576;
        if (memoryMB > 100) {
            analysis.issues.push({
                type: 'Memory Usage',
                severity: memoryMB > 200 ? 'High' : 'Medium',
                impact: memoryMB > 200 ? -20 : -10,
                details: `High memory usage: ${memoryMB.toFixed(2)}MB`
            });
            analysis.recommendations.push('Profile for memory leaks and optimize data structures');
        }
    }
    
    // 4. Check for expensive CSS selectors
    const styleSheets = Array.from(document.styleSheets);
    let complexSelectors = 0;
    
    styleSheets.forEach(sheet => {
        try {
            const rules = Array.from(sheet.cssRules || []);
            rules.forEach(rule => {
                if (rule.selectorText) {
                    // Check for expensive selectors
                    if (rule.selectorText.includes(':nth-child') ||
                        rule.selectorText.includes(':nth-of-type') ||
                        rule.selectorText.split(' ').length > 3) {
                        complexSelectors++;
                    }
                }
            });
        } catch (e) {
            // Cross-origin stylesheets
        }
    });
    
    if (complexSelectors > 20) {
        analysis.issues.push({
            type: 'CSS Performance',
            severity: 'Low',
            impact: -5,
            details: `Found ${complexSelectors} complex CSS selectors`
        });
        analysis.recommendations.push('Simplify CSS selectors for better performance');
    }
    
    // 5. Check for animation performance
    const animations = document.querySelectorAll('[style*="animation"], [style*="transition"]');
    const nonGPUAnimations = Array.from(animations).filter(el => {
        const style = el.getAttribute('style');
        return style && (style.includes('margin') || style.includes('padding') || 
                        style.includes('width') || style.includes('height'));
    });
    
    if (nonGPUAnimations.length > 0) {
        analysis.issues.push({
            type: 'Animation Performance',
            severity: 'Medium',
            impact: -10,
            details: `${nonGPUAnimations.length} elements animating non-GPU properties`
        });
        analysis.recommendations.push('Use transform and opacity for animations (GPU-accelerated)');
    }
    
    // Calculate final score
    analysis.score = Math.max(0, analysis.issues.reduce((score, issue) => 
        score + issue.impact, 100));
    
    // Add general recommendations based on score
    if (analysis.score < 70) {
        analysis.recommendations.unshift('Consider using Performance Profiler to identify bottlenecks');
    }
    
    return analysis;
}

// Run analysis
console.log(analyzePerformanceAndAdvise());

Best Practices for Performance

  1. Profile First, Optimize Second: Always measure before optimizing
  2. Focus on User-Perceived Performance: Prioritize visible improvements
  3. Avoid Premature Optimization: Profile to find actual bottlenecks
  4. Use RequestAnimationFrame: For smooth animations and visual updates
  5. Debounce and Throttle: Limit expensive operations on user input
  6. Virtual Scrolling: For large lists and data tables
  7. Web Workers: Move heavy computation off the main thread
  8. Lazy Loading: Load resources only when needed
  9. Memory Management: Explicitly null out references when done
  10. Event Delegation: Use one listener instead of many

Common Performance Issues

  • Memory Leaks: Detached DOM nodes, forgotten timers, event listeners
  • Layout Thrashing: Reading and writing DOM properties in loops
  • Forced Synchronous Layouts: Triggering layout calculations
  • Long Running Scripts: Blocking the main thread
  • Excessive DOM Size: Too many elements affecting render performance
  • Inefficient Animations: Animating properties that trigger layout
  • Large JavaScript Bundles: Loading too much code upfront
  • Unoptimized Images: Loading full-size images for thumbnails
  • Third-Party Scripts: External services impacting performance
  • Memory Bloat: Keeping too much data in memory

Remember: Performance optimization is an iterative process. Profile regularly, fix the biggest bottlenecks first, and always test the impact of your optimizations. The goal is not perfect performance, but good enough performance for your users' needs.