SalakCode SalakCode
Performance

Memory Leaks

Identify and prevent memory leaks in JavaScript

Advanced
performance memory debugging profiling

Definition

Memory leaks occur when a program continues to hold references to memory that is no longer needed. In JavaScript, this prevents the garbage collector from reclaiming memory, leading to increased memory usage, degraded performance, and eventually application crashes.

Common Causes

1. Accidental Global Variables

// ❌ Global leak
function process() {
  name = 'John'; // Implicit global - no var/let/const
  this.value = 123; // Global in non-strict mode
}

// ✅ Proper declaration
function process() {
  const name = 'John';
  const value = 123;
}

2. Forgotten Timers

// ❌ Timer keeps referencing data
function setupTimer(data) {
  setInterval(() => {
    console.log(data);
  }, 1000);
  // Never cleared - data leaked
}

// ✅ Clean up timers
function setupTimer(data) {
  const timerId = setInterval(() => {
    console.log(data);
  }, 1000);
  
  return () => clearInterval(timerId);
}

// In React
useEffect(() => {
  const timer = setInterval(...);
  return () => clearInterval(timer); // Cleanup
}, []);

3. Event Listeners

// ❌ Listener not removed
element.addEventListener('click', handleClick);
// If element removed, handleClick still referenced

// ✅ Remove listeners
element.addEventListener('click', handleClick);

// Later
element.removeEventListener('click', handleClick);

// Or use once option
element.addEventListener('click', handleClick, { once: true });

4. Closures

// ❌ Closure keeps large array in memory
function processData() {
  const hugeArray = new Array(1000000).fill('data');
  
  return function() {
    console.log('Processing');
    // hugeArray kept in memory even if not used
  };
}

// ✅ Minimize closure scope
function processData() {
  const hugeArray = new Array(1000000).fill('data');
  const result = process(hugeArray);
  
  return function() {
    console.log(result); // Only result kept
  };
}

5. DOM References

// ❌ Detached DOM nodes
function removeElement() {
  const element = document.getElementById('myElement');
  element.remove();
  // But reference still exists
  window.savedElement = element; // Leaked!
}

// ✅ Clean references
function removeElement() {
  const element = document.getElementById('myElement');
  element.remove();
  // No references kept
}

// In React - don't store DOM refs unnecessarily
const ref = useRef();
// ref.current keeps DOM node
// Set to null on unmount

6. Subscription Patterns

// ❌ Observer not unsubscribed
class EventEmitter {
  constructor() {
    this.listeners = [];
  }
  
  subscribe(listener) {
    this.listeners.push(listener);
    // No unsubscribe method!
  }
}

// ✅ Proper unsubscribe
class EventEmitter {
  subscribe(listener) {
    this.listeners.push(listener);
    
    return () => {
      const index = this.listeners.indexOf(listener);
      if (index > -1) {
        this.listeners.splice(index, 1);
      }
    };
  }
}

// Usage
const unsubscribe = emitter.subscribe(handler);
// Later
unsubscribe(); // Clean up

Detection

Chrome DevTools

1. Performance tab > Record
2. Look for continuously increasing JS Heap
3. Memory tab > Heap snapshots
4. Compare snapshots before/after suspected leak
5. Look for:
   - Detached DOM nodes
   - Increasing array/object counts
   - Retainers showing what's holding memory

Performance Monitor

// Check memory usage
console.log('Memory used:', 
  performance.memory.usedJSHeapSize / 1048576, 
  'MB'
);

// Monitor over time
setInterval(() => {
  console.log('Heap:', performance.memory.usedJSHeapSize);
}, 5000);

WeakMap/WeakSet

// ✅ Use WeakMap for private data
const privateData = new WeakMap();

class MyClass {
  constructor(data) {
    privateData.set(this, data);
    // Data automatically cleaned when instance garbage collected
  }
}

// ❌ Regular Map keeps reference
const cache = new Map();
cache.set(key, largeObject);
// Must manually delete

Prevention Patterns

Cleanup in Components

// React cleanup pattern
function MyComponent() {
  useEffect(() => {
    const controller = new AbortController();
    
    fetch('/api/data', { signal: controller.signal })
      .then(response => response.json())
      .then(data => setData(data));
    
    const handleResize = () => {
      // Update state
    };
    
    window.addEventListener('resize', handleResize);
    const timer = setInterval(poll, 5000);
    
    // Cleanup everything
    return () => {
      controller.abort();
      window.removeEventListener('resize', handleResize);
      clearInterval(timer);
    };
  }, []);
}

WeakRef (Advanced)

// WeakRef allows object to be garbage collected
const ref = new WeakRef(largeObject);

// Later
const obj = ref.deref();
if (obj) {
  // Object still exists
  obj.doSomething();
} else {
  // Object was garbage collected
}

// Use with FinalizationRegistry
const registry = new FinalizationRegistry(key => {
  console.log(`Object ${key} was garbage collected`);
});

registry.register(obj, 'myObject');

Debugging Example

// Suspected leak
class LeakyComponent {
  constructor() {
    this.data = new Array(1000000);
    
    window.addEventListener('scroll', this.handleScroll);
    // Missing: removeEventListener
  }
}

// Fixed version
class FixedComponent {
  constructor() {
    this.data = new Array(1000000);
    this.handleScroll = this.handleScroll.bind(this);
    
    window.addEventListener('scroll', this.handleScroll);
  }
  
  destroy() {
    window.removeEventListener('scroll', this.handleScroll);
    this.data = null; // Help GC
  }
}
Key Takeaway

Prevent memory leaks by cleaning up timers, event listeners, and subscriptions when components unmount. Avoid storing large objects in closures unnecessarily. Use WeakMap/WeakSet for caches that should follow object lifetime. Profile memory usage in Chrome DevTools to identify leaks early.

Resources

Related Topics