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.
Frontend Interview Team
February 08, 2026
Memory leaks are a favorite interview topic because they test two things:
- your JavaScript fundamentals (references + garbage collection)
- 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 referencedClosures 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:
WebSocketResizeObserverIntersectionObserver- 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
- Take a heap snapshot (baseline)
- Perform the action (navigate, open/close modal)
- Take another snapshot
- 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.