Back to Blog

JavaScript Memory Leaks: Detection and Prevention

Learn how to detect and prevent memory leaks in JavaScript applications using Chrome DevTools, performance.memory API, and best practices. Complete guide with real-world examples and solutions.

10 min read
By Perf Lens Team

Introduction

Memory leaks are one of the most common and challenging performance issues in JavaScript applications. Left unchecked, they can cause your application to slow down, freeze, or even crash. In this comprehensive guide, you'll learn how to detect, diagnose, and prevent memory leaks using modern tools and best practices.

What You'll Learn

This guide covers common causes of memory leaks, detection techniques using Chrome DevTools, the performance.memory API, and proven prevention strategies with real-world code examples.


What are Memory Leaks?

A memory leak occurs when your application allocates memory but fails to release it after it's no longer needed. Over time, these unreleased memory blocks accumulate, consuming more and more RAM until the application becomes unresponsive.

Why Memory Leaks Matter

  • Performance degradation: Slower response times and UI freezes
  • Increased resource usage: Higher memory consumption on user devices
  • Poor user experience: App crashes and browser tab crashes
  • SEO impact: Page speed is a ranking factor; leaks hurt Core Web Vitals

How JavaScript Manages Memory

JavaScript uses automatic garbage collection (GC) to manage memory. The GC identifies objects that are no longer reachable and frees their memory. However, if your code maintains unintended references to objects, the GC cannot reclaim that memory—causing a leak.

Loading diagram...

Common Causes of Memory Leaks

1. Accidental Global Variables

Forgetting to declare variables with const, let, or var creates global variables that persist for the entire application lifetime.

Problem:

function createUser() {
  // Missing 'const' - creates a global variable!
  user = { name: 'John', data: new Array(1000000) };
}

createUser();
// 'user' is now global and never garbage collected

Solution:

function createUser() {
  const user = { name: 'John', data: new Array(1000000) };
  return user;
}

const user = createUser();
// 'user' is scoped and can be garbage collected when no longer needed
Always Use Strict Mode

Enable strict mode with 'use strict'; at the top of your files. This prevents accidental global variable creation by throwing errors.


2. Forgotten Event Listeners

Event listeners are a common source of memory leaks. If you add listeners but never remove them, the associated DOM elements and callback functions remain in memory.

Problem:

class ChatWidget {
  constructor() {
    this.messages = [];

    // Event listener added but never removed
    document.addEventListener('click', (e) => {
      this.messages.push(e.target.textContent);
    });
  }
}

// Each new ChatWidget instance leaks memory
const widget1 = new ChatWidget();
const widget2 = new ChatWidget(); // Previous listener still active!

Solution:

class ChatWidget {
  constructor() {
    this.messages = [];
    this.handleClick = this.handleClick.bind(this);

    document.addEventListener('click', this.handleClick);
  }

  handleClick(e) {
    this.messages.push(e.target.textContent);
  }

  destroy() {
    // Always clean up event listeners
    document.removeEventListener('click', this.handleClick);
    this.messages = [];
  }
}

const widget = new ChatWidget();
// Later, when done:
widget.destroy();

3. Closures Retaining Large Objects

Closures are powerful but can accidentally hold references to large objects longer than necessary.

Problem:

function createExpensiveProcessor() {
  const hugeData = new Array(1000000).fill('data');

  return function process(input) {
    // This closure keeps 'hugeData' in memory forever,
    // even if we only need a small part of it
    return input + hugeData[0];
  };
}

const processor = createExpensiveProcessor();
// 'hugeData' is still in memory, even though we only use one element

Solution:

function createExpensiveProcessor() {
  const hugeData = new Array(1000000).fill('data');
  const firstElement = hugeData[0]; // Extract only what we need

  return function process(input) {
    // Closure only holds 'firstElement', not the entire array
    return input + firstElement;
  };
}

const processor = createExpensiveProcessor();
// 'hugeData' can be garbage collected

4. Timers and Intervals

setTimeout and setInterval callbacks can leak memory if not properly cleared.

Problem:

class DataFetcher {
  constructor() {
    this.data = [];

    // Interval never cleared
    setInterval(() => {
      this.data.push(fetch('/api/data'));
    }, 1000);
  }
}

const fetcher = new DataFetcher();
// Even if 'fetcher' is no longer used, the interval keeps running

Solution:

class DataFetcher {
  constructor() {
    this.data = [];

    this.intervalId = setInterval(() => {
      this.data.push(fetch('/api/data'));
    }, 1000);
  }

  destroy() {
    clearInterval(this.intervalId);
    this.data = [];
  }
}

const fetcher = new DataFetcher();
// Later:
fetcher.destroy();

5. Detached DOM Elements

Keeping references to DOM elements that have been removed from the document prevents them from being garbage collected.

Problem:

const cache = {};

function cacheElement(id) {
  const element = document.getElementById(id);
  cache[id] = element; // Reference kept even after removal
}

cacheElement('my-div');
document.getElementById('my-div').remove();
// 'my-div' is detached but still in memory via 'cache'

Solution:

const cache = new WeakMap();

function cacheElement(id) {
  const element = document.getElementById(id);
  cache.set(element, { id, data: 'some data' });
  // WeakMap allows GC to collect the element when it's removed
}

cacheElement('my-div');
document.getElementById('my-div').remove();
// 'my-div' can be garbage collected

Detection Techniques

1. Chrome DevTools Memory Profiler

Chrome DevTools provides powerful tools for detecting memory leaks.

Steps to detect memory leaks:

  1. Open Chrome DevTools (F12)
  2. Go to Memory tab
  3. Take a heap snapshot
  4. Perform actions in your app (e.g., open/close modal, navigate pages)
  5. Take another heap snapshot
  6. Compare snapshots to see memory growth

What to look for:

  • Detached DOM trees: DOM elements removed from the page but still in memory
  • Listeners: Event listeners not properly removed
  • Arrays/Objects growing unexpectedly: Data structures accumulating unnecessary data

2. Using performance.memory API

The performance.memory API provides real-time memory usage information (Chrome only).

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',
    });
  }
}

// Monitor every 5 seconds
setInterval(monitorMemory, 5000);

Detecting leaks with performance.memory:

function detectMemoryLeak() {
  const samples = [];
  const interval = setInterval(() => {
    if (performance.memory) {
      samples.push(performance.memory.usedJSHeapSize);

      if (samples.length > 10) {
        // Check if memory is consistently increasing
        const trend = samples.slice(-5).every((val, i, arr) =>
          i === 0 || val > arr[i - 1]
        );

        if (trend) {
          console.warn('⚠️ Potential memory leak detected!');
          console.log('Memory samples:', samples.map(s => (s / 1048576).toFixed(2) + ' MB'));
        }

        clearInterval(interval);
      }
    }
  }, 2000);
}

detectMemoryLeak();

3. Heap Snapshots Comparison

Step-by-step guide:

  1. Take a baseline snapshot when the app starts
  2. Perform a user action (e.g., open modal)
  3. Take a second snapshot
  4. Reverse the action (e.g., close modal)
  5. Force garbage collection (DevTools → Performance → Collect garbage icon)
  6. Take a third snapshot
  7. Compare snapshots: memory should return to baseline

If memory doesn't return to baseline, you have a leak.


Prevention Strategies

1. Use WeakMap and WeakSet

WeakMap and WeakSet hold "weak" references to objects, allowing them to be garbage collected even if they're still in the collection.

Example: Caching without leaks

const cache = new WeakMap();

function attachMetadata(element, data) {
  cache.set(element, data);
  // When 'element' is removed from DOM, it can be GC'd
}

const div = document.createElement('div');
attachMetadata(div, { userId: 123 });

// Later, when div is removed:
div.remove();
// 'div' and its metadata can be garbage collected

2. Cleanup in React useEffect

React's useEffect hook requires cleanup to prevent leaks.

Problem:

function UserProfile({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const interval = setInterval(() => {
      fetch(`/api/user/${userId}`).then(setData);
    }, 5000);

    // Missing cleanup!
  }, [userId]);
}

Solution:

function UserProfile({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const interval = setInterval(() => {
      fetch(`/api/user/${userId}`).then(setData);
    }, 5000);

    // Cleanup function
    return () => {
      clearInterval(interval);
    };
  }, [userId]);
}

3. Implement Proper Cleanup Patterns

Always provide cleanup methods for classes:

class ResourceManager {
  constructor() {
    this.resources = [];
    this.listeners = new Map();
    this.timers = [];
  }

  addListener(target, event, handler) {
    target.addEventListener(event, handler);
    this.listeners.set(handler, { target, event });
  }

  addTimer(callback, delay) {
    const timerId = setInterval(callback, delay);
    this.timers.push(timerId);
  }

  destroy() {
    // Clean up all listeners
    this.listeners.forEach(({ target, event }, handler) => {
      target.removeEventListener(event, handler);
    });
    this.listeners.clear();

    // Clear all timers
    this.timers.forEach(clearInterval);
    this.timers = [];

    // Clear resources
    this.resources = [];
  }
}

const manager = new ResourceManager();
manager.addListener(document, 'click', handleClick);
manager.addTimer(() => console.log('tick'), 1000);

// Later:
manager.destroy(); // All resources cleaned up

Real-World Example: Modal Dialog Leak

Problem: Memory leak in modal implementation

class Modal {
  constructor(content) {
    this.content = content;
    this.element = this.createModal();

    // Leak: event listener never removed
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') {
        this.close();
      }
    });
  }

  createModal() {
    const modal = document.createElement('div');
    modal.innerHTML = this.content;
    document.body.appendChild(modal);
    return modal;
  }

  close() {
    this.element.remove();
  }
}

// Each modal instance adds a listener that's never removed
const modal1 = new Modal('Content 1');
modal1.close(); // Listener still active!

const modal2 = new Modal('Content 2');
modal2.close(); // Now 2 listeners!

Solution: Proper cleanup

class Modal {
  constructor(content) {
    this.content = content;
    this.element = this.createModal();
    this.handleEscape = this.handleEscape.bind(this);

    document.addEventListener('keydown', this.handleEscape);
  }

  createModal() {
    const modal = document.createElement('div');
    modal.innerHTML = this.content;
    document.body.appendChild(modal);
    return modal;
  }

  handleEscape(e) {
    if (e.key === 'Escape') {
      this.close();
    }
  }

  close() {
    this.element.remove();
    document.removeEventListener('keydown', this.handleEscape);
    this.element = null;
  }
}

const modal = new Modal('Content');
modal.close(); // Fully cleaned up!

Best Practices Checklist

Memory Leak Prevention Checklist
  • ✅ Always use const, let, or var (never create accidental globals)
  • ✅ Remove event listeners when no longer needed
  • ✅ Clear timers and intervals (clearTimeout, clearInterval)
  • ✅ Use WeakMap and WeakSet for caching DOM elements
  • ✅ Implement cleanup methods for classes (destroy(), dispose())
  • ✅ Return cleanup functions in React useEffect
  • ✅ Avoid closures that retain large objects unnecessarily
  • ✅ Regularly test with Chrome DevTools Memory Profiler
  • ✅ Monitor performance.memory in production (with sampling)
  • ✅ Use ESLint rules to catch common memory leak patterns

Testing for Memory Leaks

Automated Testing

// Example: Memory leak test with Puppeteer
const puppeteer = require('puppeteer');

async function testMemoryLeak() {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto('http://localhost:3000');

  // Take baseline
  const baseline = await page.metrics();
  const baselineMemory = baseline.JSHeapUsedSize;

  // Perform actions
  for (let i = 0; i < 10; i++) {
    await page.click('#open-modal');
    await page.click('#close-modal');
  }

  // Force GC (requires --expose-gc flag)
  await page.evaluate(() => {
    if (global.gc) global.gc();
  });

  // Check memory
  const afterMetrics = await page.metrics();
  const memoryIncrease = afterMetrics.JSHeapUsedSize - baselineMemory;

  if (memoryIncrease > 5 * 1024 * 1024) { // 5MB threshold
    console.error(`Memory leak detected: ${memoryIncrease / 1024 / 1024}MB increase`);
  }

  await browser.close();
}

testMemoryLeak();

Conclusion

Memory leaks are preventable with proper coding practices and the right tools. By understanding common causes, using Chrome DevTools for detection, and implementing cleanup patterns, you can build performant JavaScript applications that run smoothly over long periods.

Key takeaways:

  • Memory leaks accumulate over time and degrade performance
  • Chrome DevTools Memory Profiler is your best friend for detection
  • Always clean up event listeners, timers, and DOM references
  • Use WeakMap and WeakSet for caching without leaks
  • Test regularly with heap snapshots and automated tests

Ready to test your code for memory leaks? Try Perf Lens to measure your JavaScript performance and detect memory issues in real-time.


Additional Resources


Last updated: October 22, 2025

JavaScript Memory Leaks: Detection and Prevention | Perf Lens