Memory Leaks in Frontend Apps: Causes, Detection, and Fixes (Interview-Ready)

A practical guide to frontend memory leaks: the most common causes (listeners, timers, DOM refs, closures), how to detect them in Chrome DevTools, and how to fix them.

F

Frontend Interview Team

February 08, 2026

~15 min
Memory Leaks in Frontend Apps: Causes, Detection, and Fixes (Interview-Ready)

Memory leaks are a favorite interview topic because they test two things:

  1. your JavaScript fundamentals (references + garbage collection)
  2. your real-world debugging ability

Good news: most frontend leaks come from a small set of patterns.

30‑second interview answer

A memory leak happens when your app keeps references to objects it no longer needs, so garbage collection can’t free them. On the frontend, leaks usually come from uncleaned event listeners, timers, retained DOM references, and long-lived closures. You detect them by reproducing a flow (like navigating a page repeatedly) and using Chrome DevTools Memory tools (heap snapshots / allocation timelines) to find objects that keep growing.

Key points

  • GC frees only unreachable objects.
  • Most leaks are “forgot to cleanup” (listeners, intervals, observers).
  • SPA navigation can amplify leaks.
  • DevTools heap snapshots help find retainers.

1) What is a memory leak (in plain English)?

A memory leak happens when:

  • your app keeps references to things it no longer needs
  • so the garbage collector can’t free them

In frontend apps, this usually shows up as:

  • the tab slowly using more RAM
  • UI becomes laggy after navigation
  • mobile devices crash/reload the page

2) The mental model: garbage collection + references

JavaScript uses garbage collection.

If an object is reachable (still referenced), it stays. If it’s unreachable, GC can clean it.

So the key question is always:

“What is still referencing this thing?”


3) The most common frontend memory leak causes

A) Event listeners not removed

function setup() {
  const onResize = () => console.log('resize');
  window.addEventListener('resize', onResize);
 
  // if you never remove it, the function reference stays alive
}

Fix: remove in cleanup.

In React:

useEffect(() => {
  const onResize = () => {};
  window.addEventListener('resize', onResize);
  return () => window.removeEventListener('resize', onResize);
}, []);

B) Timers / intervals never cleared

setInterval(() => {
  // keeps running forever
}, 1000);

Fix: store id and clear it.

const id = setInterval(fn, 1000);
// later
clearInterval(id);

React:

useEffect(() => {
  const id = setInterval(() => {}, 1000);
  return () => clearInterval(id);
}, []);

C) Detached DOM nodes referenced in JS

This is super common:

  • you remove a DOM element from the page
  • but you keep a JS reference to it
let cached;
function create() {
  const el = document.createElement('div');
  document.body.appendChild(el);
  cached = el; // strong ref
}
 
function destroy() {
  cached.remove();
  // but cached still references the detached node
}

Fix: set references to null when done.

cached = null;

D) Closures holding large objects longer than needed

function heavy() {
  const big = new Array(1_000_000).fill('x');
 
  return function handler() {
    // uses big
    console.log(big.length);
  };
}
 
const fn = heavy();
// big cannot be freed while fn is referenced

Closures are great—but they can extend lifetimes.

Fix: don’t close over large structures unless required; or release references.


E) Subscriptions not cleaned up (websockets, observers)

Examples:

  • WebSocket
  • ResizeObserver
  • IntersectionObserver
  • RxJS subscriptions

Fix: always unsubscribe/disconnect.

const observer = new ResizeObserver(() => {});
observer.observe(el);
 
// later
observer.disconnect();

4) How to detect leaks (Chrome DevTools)

Step 1: Reproduce

  • Find an action that increases memory: e.g., navigate between pages 20 times.

Step 2: Monitor memory

Open DevTools → Performance or Memory.

Practical approach:

Option A: Heap snapshots

  1. Take a heap snapshot (baseline)
  2. Perform the action (navigate, open/close modal)
  3. Take another snapshot
  4. Compare: look for growing object counts

Option B: Allocation instrumentation on timeline

  • Start recording allocations
  • do the actions
  • stop
  • inspect what keeps accumulating

Option C: Performance Monitor

DevTools → “More tools” → Performance monitor

  • track JS heap size live

Red flag: heap grows and never returns after GC.


5) What to look for in heap snapshots

Interview-friendly guidance:

  • Look for:
    • “Detached HTMLDivElement” / “Detached DOM tree”
    • huge arrays / maps
    • listener functions that keep components alive

Then ask:

  • who is retaining it?

Use Retainers panel:

  • it shows the chain of references preventing GC.

6) Common React-specific leak patterns

A) Effects without cleanup

If you create listeners/timers/subscriptions in an effect, you must clean them.

B) Setting state after unmount

Not always a “memory leak”, but it’s a sign your async work isn’t managed.

Fix with an abort signal:

useEffect(() => {
  const controller = new AbortController();
 
  fetch('/api', { signal: controller.signal }).catch(() => {});
 
  return () => controller.abort();
}, []);

7) Interview questions (and strong answers)

Q: What causes memory leaks in JS?

  • Keeping references alive (listeners, intervals, closures, DOM refs, caches).

Q: How would you debug a memory leak in a SPA?

  • Reproduce + use DevTools Memory: heap snapshots / allocation timeline.
  • Look for detached DOM trees and retainers.

Q: How do you prevent leaks?

  • Cleanups for listeners/timers/subscriptions.
  • Avoid global caches without limits.
  • Null out references to large objects when done.

Summary checklist

  • I can define a leak as “unwanted reachable references”.
  • I can list the top sources (listeners, timers, subscriptions, DOM refs, closures).
  • I can describe the DevTools approach (reproduce → heap snapshots → retainers).
  • I always clean up effects/subscriptions.

Summary

  • Most leaks come from: listeners, timers, subscriptions, DOM references, and closures.
  • Debugging: use DevTools heap snapshots + retainers.
  • Prevention: cleanup + controlled caching + avoid holding references longer than needed.