Learning Objectives

  • Understand how closures affect memory
  • Identify common memory leak patterns
  • Learn techniques to prevent memory leaks
  • Implement proper cleanup strategies

How Closures Affect Memory

Closures keep their lexical environment alive in memory. This is powerful but can lead to memory leaks if not managed properly.

function createClosure() {
    const largeData = new Array(1000000).fill('data');
    
    return function() {
        // This closure keeps largeData in memory
        return largeData.length;
    };
}

const closure = createClosure();
// largeData is still in memory even though createClosure finished!

Common Memory Leak Pattern 1: Forgotten Event Listeners

// ❌ Memory leak: Event listener never removed
function setupButton() {
    const largeData = new Array(1000000).fill('data');
    
    document.getElementById('myButton').addEventListener('click', function() {
        console.log(largeData.length);
    });
}

setupButton();
// largeData stays in memory forever!

// ✅ Solution: Remove event listener when done
function setupButton() {
    const largeData = new Array(1000000).fill('data');
    const button = document.getElementById('myButton');
    
    const handler = function() {
        console.log(largeData.length);
    };
    
    button.addEventListener('click', handler);
    
    // Return cleanup function
    return function cleanup() {
        button.removeEventListener('click', handler);
    };
}

const cleanup = setupButton();
// Later, when done:
cleanup(); // Now largeData can be garbage collected

Common Memory Leak Pattern 2: Timers

// ❌ Memory leak: Timer never cleared
function startPolling() {
    const largeData = new Array(1000000).fill('data');
    
    setInterval(function() {
        console.log(largeData.length);
    }, 1000);
}

startPolling();
// largeData and interval run forever!

// ✅ Solution: Return cleanup function
function startPolling() {
    const largeData = new Array(1000000).fill('data');
    
    const intervalId = setInterval(function() {
        console.log(largeData.length);
    }, 1000);
    
    return function stopPolling() {
        clearInterval(intervalId);
    };
}

const stop = startPolling();
// Later:
stop(); // Clears interval and allows garbage collection

Common Memory Leak Pattern 3: Circular References

// ❌ Potential memory leak: Circular reference
function createCircular() {
    const obj1 = {};
    const obj2 = {};
    
    obj1.ref = obj2;
    obj2.ref = obj1;
    
    return function() {
        return obj1;
    };
}

// Modern JavaScript engines handle this, but be aware

// ✅ Better: Break references when done
function createCircular() {
    let obj1 = {};
    let obj2 = {};
    
    obj1.ref = obj2;
    obj2.ref = obj1;
    
    return {
        get: function() {
            return obj1;
        },
        cleanup: function() {
            obj1.ref = null;
            obj2.ref = null;
            obj1 = null;
            obj2 = null;
        }
    };
}

Pattern: Cleanup Function

Always provide a way to clean up closures:

function createSubscription(topic, callback) {
    const listeners = [];
    
    // Subscribe
    listeners.push(callback);
    
    // Return unsubscribe function
    return function unsubscribe() {
        const index = listeners.indexOf(callback);
        if (index > -1) {
            listeners.splice(index, 1);
        }
    };
}

const unsubscribe = createSubscription('news', (data) => {
    console.log(data);
});

// When done:
unsubscribe(); // Allows garbage collection

Pattern: WeakMap for Private Data

Use WeakMap to allow garbage collection:

// Using WeakMap allows garbage collection
const privateData = new WeakMap();

function createObject(data) {
    const obj = {};
    
    // Store private data
    privateData.set(obj, data);
    
    return {
        getData: function() {
            return privateData.get(obj);
        }
    };
}

let myObj = createObject({ secret: 'value' });
// When myObj is no longer referenced, both it and its
// private data can be garbage collected

myObj = null; // Private data can now be collected

Pattern: Nullifying References

function createCache() {
    let cache = new Map();
    let isActive = true;
    
    return {
        set: function(key, value) {
            if (isActive) {
                cache.set(key, value);
            }
        },
        
        get: function(key) {
            if (isActive) {
                return cache.get(key);
            }
        },
        
        destroy: function() {
            cache.clear();
            cache = null; // Allow garbage collection
            isActive = false;
        }
    };
}

const cache = createCache();
cache.set('key', 'value');

// When done:
cache.destroy(); // Cleans up and allows GC

Detecting Memory Leaks

// Monitor memory usage
function monitorMemory() {
    if (performance.memory) {
        console.log({
            usedJSHeapSize: (performance.memory.usedJSHeapSize / 1048576).toFixed(2) + ' MB',
            totalJSHeapSize: (performance.memory.totalJSHeapSize / 1048576).toFixed(2) + ' MB',
            jsHeapSizeLimit: (performance.memory.jsHeapSizeLimit / 1048576).toFixed(2) + ' MB'
        });
    }
}

// Check before and after operations
monitorMemory();
// ... perform operations ...
monitorMemory();

Best Practices for Memory Management

1. Limit Closure Scope

// ❌ Captures unnecessary data
function createHandler() {
    const largeData = loadLargeData();
    const smallData = extractSmallData(largeData);
    
    return function() {
        // Only needs smallData, but captures largeData too!
        return smallData;
    };
}

// ✅ Better: Limit what's captured
function createHandler() {
    const largeData = loadLargeData();
    const smallData = extractSmallData(largeData);
    // largeData goes out of scope here
    
    return function() {
        return smallData; // Only captures smallData
    };
}

2. Use Weak References

// Use WeakMap/WeakSet for object keys
const cache = new WeakMap();

function cacheResult(obj, result) {
    cache.set(obj, result);
}

let myObj = { id: 1 };
cacheResult(myObj, 'result');

// When myObj is no longer referenced, it and its
// cached result can be garbage collected
myObj = null;

3. Implement Disposal Pattern

class ResourceManager {
    constructor() {
        this.resources = [];
        this.isDisposed = false;
    }
    
    addResource(resource) {
        if (this.isDisposed) {
            throw new Error('Manager is disposed');
        }
        this.resources.push(resource);
    }
    
    dispose() {
        if (this.isDisposed) return;
        
        // Clean up all resources
        this.resources.forEach(resource => {
            if (resource.cleanup) {
                resource.cleanup();
            }
        });
        
        this.resources = [];
        this.isDisposed = true;
    }
}

const manager = new ResourceManager();
// ... use manager ...
manager.dispose(); // Clean up when done

4. Avoid Accidental Globals

// ❌ Accidental global
function createClosure() {
    // Missing 'const/let/var' creates global!
    data = new Array(1000000);
    
    return function() {
        return data.length;
    };
}

// ✅ Always declare variables
function createClosure() {
    const data = new Array(1000000);
    
    return function() {
        return data.length;
    };
}

Practical Example: Event Manager with Cleanup

function createEventManager() {
    const listeners = new Map();
    
    return {
        on: function(event, callback) {
            if (!listeners.has(event)) {
                listeners.set(event, new Set());
            }
            listeners.get(event).add(callback);
            
            // Return unsubscribe function
            return () => {
                const callbacks = listeners.get(event);
                if (callbacks) {
                    callbacks.delete(callback);
                    if (callbacks.size === 0) {
                        listeners.delete(event);
                    }
                }
            };
        },
        
        emit: function(event, data) {
            const callbacks = listeners.get(event);
            if (callbacks) {
                callbacks.forEach(callback => callback(data));
            }
        },
        
        removeAllListeners: function(event) {
            if (event) {
                listeners.delete(event);
            } else {
                listeners.clear();
            }
        },
        
        destroy: function() {
            listeners.clear();
        }
    };
}

const events = createEventManager();
const unsubscribe = events.on('data', (data) => console.log(data));

// Clean up when done
unsubscribe();
// Or destroy everything
events.destroy();

Practical Example: Component Lifecycle

function createComponent(element) {
    const cleanupFunctions = [];
    let isDestroyed = false;
    
    function registerCleanup(fn) {
        cleanupFunctions.push(fn);
    }
    
    // Setup event listeners
    const clickHandler = () => console.log('clicked');
    element.addEventListener('click', clickHandler);
    registerCleanup(() => {
        element.removeEventListener('click', clickHandler);
    });
    
    // Setup interval
    const intervalId = setInterval(() => {
        console.log('tick');
    }, 1000);
    registerCleanup(() => {
        clearInterval(intervalId);
    });
    
    return {
        destroy: function() {
            if (isDestroyed) return;
            
            // Run all cleanup functions
            cleanupFunctions.forEach(fn => fn());
            cleanupFunctions.length = 0;
            
            isDestroyed = true;
        }
    };
}

const component = createComponent(document.getElementById('myElement'));
// When component is no longer needed:
component.destroy();

Tools for Detecting Memory Leaks

  • Chrome DevTools: Memory profiler and heap snapshots
  • Performance.memory API: Monitor heap size
  • Memory Timeline: Track allocations over time
  • Heap Snapshots: Compare before/after states

Key Takeaways

  • ✅ Closures keep their lexical environment alive in memory
  • ✅ Always remove event listeners and clear timers
  • ✅ Provide cleanup/dispose functions for closures
  • ✅ Use WeakMap/WeakSet for automatic garbage collection
  • Nullify references when done with large objects
  • ✅ Use browser DevTools to detect memory leaks

Next Steps

Now that you understand memory management with closures, in the final lesson we'll explore performance considerations and optimization techniques.