Back to patterns

Memory Leaks 101

PERFORMANCE

A systematic approach to identifying and fixing memory leaks through heap snapshot analysis, memory usage visualization, and common leak pattern detection.

Diagram for Memory Leaks 101

Use Cases

  • Long-running web applications
  • Single-page applications (SPAs)
  • Real-time data processing systems
  • Applications with dynamic component loading

Production Implementation

Implementation
// Real-world example: Memory leak detection and prevention in a React-like application
interface Subscription {
  unsubscribe(): void;
}

interface CacheOptions {
  maxSize?: number;
  ttl?: number;
}

// Memory-safe cache implementation with size and TTL limits
class LRUCache<K, V> {
  private cache: Map<K, { value: V; timestamp: number }>;
  private maxSize: number;
  private ttl: number;

  constructor(options: CacheOptions = {}) {
    this.cache = new Map();
    this.maxSize = options.maxSize || 1000;
    this.ttl = options.ttl || 3600000; // 1 hour default
    
    // Periodic cleanup to prevent memory leaks
    setInterval(() => this.cleanup(), 60000);
  }

  set(key: K, value: V): void {
    // Enforce size limit
    if (this.cache.size >= this.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }

    this.cache.set(key, {
      value,
      timestamp: Date.now()
    });
  }

  get(key: K): V | undefined {
    const item = this.cache.get(key);
    
    if (!item) return undefined;

    // Check TTL
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key);
      return undefined;
    }

    return item.value;
  }

  private cleanup(): void {
    const now = Date.now();
    for (const [key, item] of this.cache.entries()) {
      if (now - item.timestamp > this.ttl) {
        this.cache.delete(key);
      }
    }
  }
}

// Memory-safe event emitter with automatic cleanup
class EventEmitter {
  private events: Map<string, Set<WeakRef<Function>>>;
  private cleanupInterval: number;

  constructor() {
    this.events = new Map();
    
    // Periodic cleanup of dead references
    this.cleanupInterval = setInterval(() => this.cleanup(), 30000);
  }

  on(event: string, callback: Function): Subscription {
    if (!this.events.has(event)) {
      this.events.set(event, new Set());
    }

    const callbacks = this.events.get(event)!;
    const weakCallback = new WeakRef(callback);
    callbacks.add(weakCallback);

    return {
      unsubscribe: () => {
        callbacks.delete(weakCallback);
        if (callbacks.size === 0) {
          this.events.delete(event);
        }
      }
    };
  }

  emit(event: string, ...args: any[]): void {
    const callbacks = this.events.get(event);
    if (!callbacks) return;

    for (const weakCallback of callbacks) {
      const callback = weakCallback.deref();
      if (callback) {
        callback(...args);
      } else {
        callbacks.delete(weakCallback);
      }
    }
  }

  private cleanup(): void {
    for (const [event, callbacks] of this.events.entries()) {
      for (const weakCallback of callbacks) {
        if (!weakCallback.deref()) {
          callbacks.delete(weakCallback);
        }
      }
      if (callbacks.size === 0) {
        this.events.delete(event);
      }
    }
  }

  destroy(): void {
    clearInterval(this.cleanupInterval);
    this.events.clear();
  }
}

// Memory-safe component base class
abstract class Component {
  private subscriptions: Set<Subscription>;
  private timers: Set<number>;
  private eventEmitter: EventEmitter;
  private mounted: boolean;

  constructor() {
    this.subscriptions = new Set();
    this.timers = new Set();
    this.eventEmitter = new EventEmitter();
    this.mounted = false;
  }

  protected onMount(): void {
    this.mounted = true;
  }

  protected onUnmount(): void {
    this.mounted = false;
    this.cleanup();
  }

  // Safe subscription management
  protected subscribe(subscription: Subscription): void {
    this.subscriptions.add(subscription);
  }

  // Safe timer management
  protected setTimeout(callback: () => void, delay: number): number {
    const id = window.setTimeout(() => {
      if (this.mounted) {
        callback();
      }
      this.timers.delete(id);
    }, delay);
    
    this.timers.add(id);
    return id;
  }

  protected setInterval(callback: () => void, delay: number): number {
    const id = window.setInterval(() => {
      if (!this.mounted) {
        clearInterval(id);
        this.timers.delete(id);
        return;
      }
      callback();
    }, delay);
    
    this.timers.add(id);
    return id;
  }

  // Safe event listener management
  protected addEventListener(
    target: EventTarget,
    type: string,
    listener: EventListener,
    options?: AddEventListenerOptions
  ): void {
    target.addEventListener(type, listener, options);
    this.subscriptions.add({
      unsubscribe: () => target.removeEventListener(type, listener)
    });
  }

  private cleanup(): void {
    // Clear subscriptions
    for (const subscription of this.subscriptions) {
      subscription.unsubscribe();
    }
    this.subscriptions.clear();

    // Clear timers
    for (const timer of this.timers) {
      clearTimeout(timer);
    }
    this.timers.clear();

    // Cleanup event emitter
    this.eventEmitter.destroy();
  }
}

// Example usage
class DataGrid extends Component {
  private cache: LRUCache<string, any>;
  
  constructor() {
    super();
    this.cache = new LRUCache({ maxSize: 100, ttl: 300000 }); // 5 minutes TTL
  }

  protected onMount(): void {
    super.onMount();

    // Safe event listener
    this.addEventListener(window, 'resize', this.handleResize);

    // Safe interval
    this.setInterval(this.refreshData, 60000);

    // Safe subscription
    const subscription = dataService.subscribe(
      this.handleDataUpdate
    );
    this.subscribe(subscription);
  }

  private handleResize = () => {
    // Handle resize logic
  };

  private refreshData = () => {
    // Refresh data logic
  };

  private handleDataUpdate = (data: any) => {
    this.cache.set('latestData', data);
  };
}

Code Examples

Common Memory Leak Patterns

typescript
// ❌ Common memory leak patterns
class DataComponent {
  private eventHandlers = new Set();
  private cache = {};
  
  constructor() {
    // Issue 1: Unbounded cache growth
    this.cache = {};
    
    // Issue 2: Event listener not removed
    window.addEventListener('resize', this.handleResize);
    
    // Issue 3: Interval not cleared
    setInterval(this.fetchData, 5000);
    
    // Issue 4: Closure keeping reference
    const data = { value: 'test' };
    this.eventHandlers.add(() => {
      console.log(data.value);
    });
  }
  
  handleResize = () => {
    // Handle resize
  };
  
  fetchData = () => {
    // Fetch and cache data
    this.cache[Date.now()] = 'data';
  };
}

Common memory leaks include unbounded caches, uncleared intervals, uncleaned event listeners, and closure references.

Memory-Safe Implementation

typescript
// ✅ Memory-safe implementation
class DataComponent {
  private eventHandlers = new Set<() => void>();
  private cache = new LRUCache<string, any>({ maxSize: 100 });
  private intervalId?: number;
  
  constructor() {
    // Bounded LRU cache
    this.cache = new LRUCache({ maxSize: 100, ttl: 3600000 });
    
    // Tracked event listener
    this.addEventHandler(
      'resize',
      this.handleResize
    );
    
    // Tracked interval
    this.intervalId = window.setInterval(this.fetchData, 5000);
  }
  
  private addEventHandler(
    event: string,
    handler: EventListener
  ): void {
    window.addEventListener(event, handler);
    this.eventHandlers.add(
      () => window.removeEventListener(event, handler)
    );
  }
  
  handleResize = () => {
    // Handle resize
  };
  
  fetchData = () => {
    // Cache with TTL
    this.cache.set(Date.now().toString(), 'data');
  };
  
  destroy(): void {
    // Clean up event listeners
    for (const cleanup of this.eventHandlers) {
      cleanup();
    }
    this.eventHandlers.clear();
    
    // Clear interval
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
    
    // Clear cache
    this.cache.clear();
  }
}

This implementation uses bounded caches, tracks and cleans up event listeners, and properly manages intervals.

Best Practices

  • Use WeakMap/WeakSet for caching object references
  • Implement size limits and TTL for caches
  • Track and clean up all event listeners
  • Clear intervals and timeouts on component destruction
  • Use weak references for event callbacks
  • Implement proper cleanup methods
  • Monitor memory usage in development

Common Pitfalls

  • Unbounded caches leading to memory growth
  • Uncleaned event listeners in SPAs
  • Forgotten setInterval cleanup
  • Circular references in data structures
  • Closure variables keeping references alive
  • Not implementing proper cleanup methods