Volume 4

Q223: Memory Leaks, Garbage Collection and Runtime Retention

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

Interviewers want to know whether you can diagnose memory problems in long-running frontend applications.

They check garbage collection, retained references, event listeners, timers, subscriptions, caches, detached DOM nodes, React effects, heap snapshots, and prevention patterns.

Short Interview Answer

A memory leak happens when objects that are no longer needed remain reachable, so the garbage collector cannot reclaim them. In frontend apps, leaks often come from event listeners, timers, subscriptions, caches, detached DOM nodes, stale closures, and uncleaned effects. I debug leaks with heap snapshots, allocation timelines, route transition reproduction, and retained-object analysis, then fix the ownership and cleanup path.

Detailed Interview Answer

JavaScript garbage collection is reachability-based.

If an object can still be reached from roots, it stays alive.

Roots include:

global objects
active stack frames
closures
DOM references
event listeners
timers
module-level caches
worker references

A leak is often accidental reachability.

Common Leak Sources

Frontend leak sources include:

window event listeners not removed
intervals not cleared
websocket subscriptions not closed
large arrays in module caches
detached DOM nodes referenced by JS
stale closures holding old data
unbounded memoization maps
third-party widgets not destroyed
workers not terminated

React apps are not immune.

React Effect Cleanup

Effects should clean up owned resources.

useEffect(() => {
  const handleResize = () => {
    console.log(window.innerWidth);
  };

  window.addEventListener("resize", handleResize);

  return () => {
    window.removeEventListener("resize", handleResize);
  };
}, []);

If the listener is not removed, the callback can keep component data alive.

Timers

Timers can retain closures.

useEffect(() => {
  const id = window.setInterval(() => {
    refreshData();
  }, 1000);

  return () => window.clearInterval(id);
}, []);

Intervals are especially risky because they repeat forever.

Subscriptions

Subscriptions need explicit ownership.

useEffect(() => {
  const unsubscribe = store.subscribe(handleChange);
  return unsubscribe;
}, [store]);

If a component subscribes, it should also unsubscribe.

If ownership is shared, document who closes the resource.

Detached DOM Nodes

A detached DOM node is removed from the document but still referenced by JavaScript.

Example:

const savedNodes: HTMLElement[] = [];

function remember(node: HTMLElement) {
  savedNodes.push(node);
}

If the node is later removed from DOM but still in savedNodes, it remains in memory.

This is common with custom widgets and third-party libraries.

Cache Leaks

Caches need limits.

Weak:

const cache = new Map<string, LargeResult>();

If keys grow forever, memory grows forever.

Better:

size limit
time-to-live
least recently used eviction
manual invalidation
WeakMap when object-key lifecycle should control retention

Caching without eviction is often a leak with better branding.

Stale Closures

Closures can retain old values.

function createHandler(largeData: LargeData) {
  return () => {
    console.log(largeData.id);
  };
}

If the handler is registered globally and never removed, largeData stays alive.

This is why cleanup matters.

Heap Snapshot Workflow

A practical workflow:

open page
take baseline heap snapshot
perform route or interaction repeatedly
force garbage collection if tooling allows
take second snapshot
compare retained objects
inspect retaining paths
identify owner
fix cleanup or cache policy
repeat verification

The retaining path tells you why an object is still reachable.

Allocation Timeline

Use allocation timelines for leaks that grow over time.

Look for:

memory steadily climbing
objects not dropping after route changes
listener counts increasing
large arrays retained
detached nodes accumulating
worker memory not released

One snapshot is useful.

Growth over repeated actions is stronger evidence.

Common Mistakes

Common mistakes include:

assuming garbage collection means leaks are impossible
forgetting cleanup in effects
using unbounded maps
not destroying third-party widgets
capturing large objects in global callbacks
debugging memory without reproducing repeated navigation
confusing high memory usage with a leak

High memory is not always a leak.

A leak is retained memory that should have become unreachable.

Senior Trade-Offs

Some memory retention is intentional.

Examples:

route cache
image cache
compiled search index
virtualized list measurements
recent API results

The question is whether retention has ownership, limits, and invalidation.

If not, it becomes risk.

Interview Framing

In an interview, say:

I debug leaks by reproducing growth, comparing heap snapshots, inspecting retaining paths, and fixing the owner that keeps objects reachable. In React, I look closely at effects, subscriptions, timers, caches, and third-party cleanup.

Revision Notes

  • Garbage collection is based on reachability.
  • Leaks are unwanted retained references.
  • Effects should clean up owned resources.
  • Caches need limits and invalidation.
  • Heap snapshots show retaining paths.
  • Repeated interaction reproduction is key.

Follow-Up Questions

  • What makes an object eligible for garbage collection?
  • How can an event listener cause a leak?
  • What is a detached DOM node?
  • How would you debug memory growth after route changes?
  • When is a cache not a leak?

How I Would Answer This In A Real Interview

I would say a frontend memory leak usually means something remains reachable after the user leaves a page or finishes an interaction. I would reproduce the growth, take heap snapshots, compare retained objects, inspect retaining paths, and then fix the cleanup or cache ownership. In React, I would start with effects, listeners, timers, subscriptions, stale closures, and third-party widgets.