Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to avoid memory leaks in JavaScript

Memory leaks cause applications to consume increasing amounts of memory over time, leading to poor performance and crashes. As the creator of CoreUI with over 25 years of JavaScript experience since 2000, I’ve debugged memory leaks in long-running single-page applications serving millions of users. The most common causes are forgotten event listeners, unclosed timers, and circular references in closures. Proper cleanup in component lifecycles prevents these issues.

Remove event listeners when elements are removed.

// ❌ Memory leak - listener never removed
const button = document.getElementById('myButton')
button.addEventListener('click', handleClick)
button.remove() // Button gone but listener remains in memory

// ✅ Proper cleanup
const button = document.getElementById('myButton')
const handleClick = () => console.log('Clicked')
button.addEventListener('click', handleClick)

// Later, before removing element
button.removeEventListener('click', handleClick)
button.remove()

Event listeners keep references to DOM elements. Removing elements without removing listeners causes leaks. Always call removeEventListener before removing elements. Store function references to enable removal.

Clear Timers and Intervals

Cancel timers when components unmount.

// ❌ Memory leak - interval keeps running
function startPolling() {
  setInterval(() => {
    fetch('/api/status').then(updateUI)
  }, 1000)
}

// ✅ Proper cleanup
function startPolling() {
  const intervalId = setInterval(() => {
    fetch('/api/status').then(updateUI)
  }, 1000)

  return () => clearInterval(intervalId)
}

const cleanup = startPolling()
// Later, when done
cleanup()

Timers continue running even after the component is gone. Store timer IDs to clear them later. Use cleanup functions in frameworks. This prevents accumulating background tasks.

Avoid Closure Memory Leaks

Be careful with closures holding large objects.

// ❌ Memory leak - closure holds entire data array
function createHandler(data) {
  return function() {
    console.log(data[0])
  }
}
const handler = createHandler(hugeArray) // hugeArray stays in memory

// ✅ Extract only needed data
function createHandler(data) {
  const firstItem = data[0]
  return function() {
    console.log(firstItem)
  }
}
const handler = createHandler(hugeArray) // Only firstItem kept

Closures retain references to outer scope variables. Extract only needed values before creating closures. This prevents holding large objects unnecessarily.

Remove DOM References

Clear variables pointing to removed DOM elements.

// ❌ Memory leak - detached DOM elements kept in memory
let cachedElements = []

function cacheElements() {
  cachedElements = Array.from(document.querySelectorAll('.item'))
}

function removeItems() {
  cachedElements.forEach(el => el.remove())
  // cachedElements still references removed elements
}

// ✅ Proper cleanup
function removeItems() {
  cachedElements.forEach(el => el.remove())
  cachedElements = [] // Clear references
}

Detached DOM nodes stay in memory if JavaScript holds references. Clear arrays and objects containing removed elements. Use WeakMap for element-keyed caches.

Clean Up Framework Resources

Properly dispose of framework subscriptions.

// React example
function MyComponent() {
  useEffect(() => {
    const subscription = dataSource.subscribe(handleData)

    // Cleanup function
    return () => {
      subscription.unsubscribe()
    }
  }, [])

  return <div>Content</div>
}

// Vue example
export default {
  mounted() {
    this.subscription = dataSource.subscribe(this.handleData)
  },
  beforeUnmount() {
    this.subscription.unsubscribe()
  }
}

Framework components must clean up subscriptions. React useEffect returns a cleanup function. Vue uses lifecycle hooks. Angular uses ngOnDestroy. Always unsubscribe from observables and event emitters.

Detecting Memory Leaks

Use Chrome DevTools to find leaks.

// Take heap snapshot before action
// Perform action (navigate, create/destroy component)
// Force garbage collection
// Take second heap snapshot
// Compare snapshots

// Look for:
// - Detached DOM nodes
// - Growing arrays
// - Event listeners count

Heap snapshots show memory allocations. Compare snapshots to find growing objects. The Detached DOM tree view shows orphaned elements. The Memory profiler tracks allocations over time. Profile before optimizing.

Best Practice Note

This is the same memory management approach we use in CoreUI to prevent leaks in long-running applications. Always clean up in component destruction - every addEventListener needs a removeEventListener, every setInterval needs clearInterval. Use browser DevTools regularly to profile memory usage. WeakMap and WeakSet allow garbage collection of keys. For large applications, implement automated memory leak testing in CI. Modern frameworks handle most cleanup automatically, but be careful with manual DOM manipulation and global event listeners.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author