cswork / libraryOpen practice workspace

Articles

Frontend Memory Leak Interview Questions: Detached Nodes, Listeners, and Heap Snapshots

Prepare for frontend memory leak interviews: trace detached nodes, clean up listeners, compare heap snapshots, and verify fixes without misleading claims.

A strong frontend memory leak interview answer starts with a repeatable lifecycle: open a view, use it, close it, and return to the same idle state. Compare what remains reachable after repeated cycles, then follow the retaining path to the owner that should have released it. A rising memory graph is a symptom; it is not yet a diagnosis.

Official facts in this guide come from Chrome DevTools, MDN, and React documentation. Candidate report: an August 2026 Anduril interview account describes a JavaScript memory-debugging question and a response involving CDP, reproduction, and comparison. That single account does not establish a standard interview sequence. Our inference: the most useful preparation is to explain ownership and demonstrate comparable before-and-after evidence.

Does growing browser memory prove a leak?

No. An application can allocate heavily and later reclaim objects. It can also retain a legitimate working set, such as a bounded image cache. A leak occurs when objects remain retained beyond their intended useful lifetime, often producing continued growth as users repeat a workflow.

Chrome distinguishes leaks, memory bloat, and frequent garbage collection in its memory troubleshooting documentation. Those problems can feel similar to a user but require different repairs. Reducing allocations may improve pauses without fixing a retaining path. Evicting everything may reduce memory while breaking useful caching.

Start by identifying which measurement is growing. JavaScript heap size, DOM node count, process memory, and GPU resources describe different parts of the system. A heap snapshot is not a complete inventory of every byte attributed to a browser process.

In an interview, ask what action triggers the growth and whether memory stabilizes after warm-up. State a provisional hypothesis rather than calling every upward slope a leak. Your next measurement should distinguish plausible causes instead of simply producing a larger chart.

Why can a detached DOM node stay alive?

A detached node is no longer connected to the document tree. That does not mean it is unreachable from JavaScript. A cache, callback, debugging reference, or retained parent can still keep it and its descendants accessible.

The MDN memory-management guide explains reachability-based collection. The useful question is not whether an object participates in a cycle; it is whether a live root can still reach the object. Modern tracing collection can reclaim an unreachable cycle.

Consider this original example:

function mountPanel(host) {
  const panel = document.createElement("section");
  panel.textContent = "Preview";
  host.append(panel);

  const onResize = () => {
    panel.style.width = `${window.innerWidth / 2}px`;
  };
  window.addEventListener("resize", onResize);

  return () => panel.remove(); // incomplete cleanup
}

Removing the panel cuts its document-tree relationship. It does not remove the callback registered on window. That callback still needs panel when a resize occurs. The conceptual path is window → listener → callback environment → panel.

Do not generalize this to “every detached node leaks.” A library may intentionally retain a detached node for reuse, and an unreachable node can be collected. The diagnosis requires both a retaining path and a reason the object's intended lifetime has ended.

How would you repair listener ownership?

Keep the same callback identity and remove the registration when the owner ends:

return () => {
  window.removeEventListener("resize", onResize);
  panel.remove();
};

The removal must match the event type, listener function, and capture setting. Creating a new arrow function during cleanup does not refer to the original function. MDN documents these matching rules in removeEventListener.

An abort signal can group listeners under a lifecycle owner:

const lifetime = new AbortController();
window.addEventListener("resize", onResize, {
  signal: lifetime.signal,
});

return () => {
  lifetime.abort();
  panel.remove();
};

This removes listeners registered with that signal when it aborts. It does not clear arbitrary timers, erase global maps, or cancel every asynchronous library operation. Explicitly list which resources the controller owns.

There is another subtlety: the returned cleanup function itself closes over panel. If a global collection keeps all old cleanup functions forever, removing the listener may leave a different retaining path. Releasing one edge is sufficient only when no unwanted strong path remains.

Our interview recommendation is to pair each acquisition with its owner and release operation. That makes cleanup review concrete and exposes “the framework will handle it” assumptions.

What does a useful heap-snapshot comparison look like?

Use equivalent checkpoints. Our original rehearsal protocol starts after the application's ordinary initial loading and cache warm-up. Take a baseline snapshot, repeat one view lifecycle several times, return to the same idle route, and take another snapshot. Repeat the cycle again before deciding that growth is persistent.

Chrome's heap snapshot documentation describes Summary, Comparison, and related views. Comparison helps locate object-count and size changes. The retainer view helps explain why a selected object is still reachable. Allocation location and retention ownership answer different questions.

CheckpointKeep constantQuestion it answers
Baseline after warm-upRoute, dataset, browser buildWhat is the normal retained state?
After repeated open/close cyclesSame actions and idle conditionWhich objects accumulate?
After another equal batchSame workload sizeDoes growth continue or plateau?
After repairSame reproduction and controlsDid the unwanted path disappear?

The number of repetitions is a diagnostic choice, not a universal threshold. Choose enough cycles to expose the suspected object population without making the browser unusable. Record that number so another person can repeat the investigation.

Do not invent before-and-after megabyte values in your answer. If you have no measurement, describe the observation you expect and what would falsify the hypothesis. Evidence-aware uncertainty is more useful than a precise-looking number with no experiment behind it.

How do shallow size and retained size differ?

Shallow size concerns the object itself. Retained size concerns memory that could become reclaimable if that object and its exclusive retention were removed. Chrome's snapshot tooling distinguishes these concepts; a small callback can be associated with a much larger retained structure.

Use retained size to prioritize investigation, but do not sum overlapping retained-size entries as though they were independent allocations. Shared subgraphs and multiple paths complicate attribution. The largest object is not automatically the faulty owner.

Suppose a small registry entry retains an entire abandoned editor model. Deleting one large array inside the model may reduce memory while leaving the broken registration intact. Following the path back to the registry can reveal the lifecycle mistake that recreates the problem on every mount.

Conversely, a retained object may be legitimate. Ask whether the cache has a capacity, whether eviction releases values, and whether retained memory stabilizes at that bound. A label such as “cache” is not proof that growth is intentional or controlled.

Which lifecycle owners need explicit cleanup?

Different APIs have different ownership contracts. This original review matrix helps you inspect a component without treating all resources as event listeners.

Resource or ownerRelease or bound to verify
Global event listenerMatching removal or lifecycle abort signal
Repeating timerClear the interval when its owner ends
SubscriptionInvoke the library's unsubscribe operation
ObserverDisconnect or unobserve according to the intended scope
Object URLRevoke it when consumers no longer need it
Application cacheEnforce an explicit capacity and eviction policy
Cleanup registryRemove completed owners and their closures

This table is a review aid, not a statement that each API always leaks. A short-lived event target and its listeners can become unreachable together. The danger is often registering a short-lived view with a longer-lived owner.

Weak collections can be appropriate when metadata should not keep an object key alive. They do not replace a cache policy when you need enumeration, deterministic eviction, or predictable capacity. Likewise, finalization timing is unsuitable for correctness-critical resource release. MDN explains why garbage-collection timing should not become an application guarantee.

If an interviewer suggests WeakRef as the fix, ask which ownership requirement it satisfies. Making access nondeterministic can hide a design problem rather than solve the user-visible lifecycle.

What changes in React?

React's useEffect reference says cleanup runs before a changed Effect is set up again and when the component unmounts. In development Strict Mode, React performs an extra setup-and-cleanup cycle to stress-test the Effect's behavior.

An Effect that subscribes should return the corresponding cleanup. Include relevant dependencies so the subscription reflects the current input. Suppressing dependencies can retain stale behavior; resubscribing without cleanup can accumulate owners. These are separate failure modes even when the same callback is involved.

Do not assume an extra development subscription proves a production leak. Verify whether cleanup mirrors setup and whether the production lifecycle still accumulates resources. Removing Strict Mode to make the symptom disappear discards a useful diagnostic signal.

Also distinguish a stale asynchronous update from permanent retention. A pending operation may temporarily retain its callback and data until it settles. Ignoring an obsolete result protects visible state; cancelling supported work may shorten its lifetime. Neither action, by itself, proves that every external resource or reference was released.

A defensible answer explains the Effect's owner, what starts, what stops, and how a dependency change affects both. “Add an empty dependency array” is not a general memory fix.

How can the debugging process mislead you?

The act of observing can change retention. Logging large objects, keeping references in the console, or selecting objects for inspection can preserve data that would otherwise disappear. Chrome's snapshot documentation discusses objects retained through developer-console interactions.

Use a clean, documented reproduction and avoid storing investigated objects in convenience globals. If a suspicious path points to a debugging tool, repeat the investigation with that reference removed. Do not alter several application features at once and then attribute every improvement to the listener fix.

Warm-up also matters. Code loading, parsed templates, font data, and caches may legitimately increase the initial working set. Comparing a cold baseline with a warmed application can overstate a leak. Establish the steady workflow before measuring repeated growth.

Finally, forcing collection in a diagnostic environment is an observation aid, not production code. An application should not depend on manually triggering the collector at a particular moment. The durable repair changes ownership or bounds retention.

What should you show after the fix?

Repeat the original scenario with the same browser build, data, and checkpoints. Look for the previously accumulating objects and inspect whether the unwanted retaining path still exists. A stable plateau is useful evidence, but phrase its scope precisely: the repaired workflow stabilized in the tested environment.

Then verify behavior. A listener that never registers cannot leak through that registration, but the panel may no longer resize. Test mount, interaction, unmount, remount, and repeated cleanup where the API promises idempotence. Check that old views stop receiving events while the active view still does.

For automation, separate lifecycle assertions from memory measurements. Listener or subscription counters can establish that setup and teardown balance. Heap comparisons can investigate actual retained objects. Neither alone proves that the whole application is leak-free.

Your final explanation should connect symptom, evidence, root owner, repair, and residual uncertainty. For example: “Closed panels accumulated after each cycle. A window callback retained them. Cleanup now unregisters that callback; the same lifecycle no longer accumulates those panels, and resize still works on the active panel.” This is an answer structure, not a claimed measurement from a real production incident.

Practice ownership, diagnosis, and bounded retention

QuestionFocus for this article
Diagnose a Memory Leak in a JavaScript Web ApplicationRepeat a lifecycle and follow retaining paths
Debug and optimize React performance issuesSeparate rendering cost from resource retention
Implement an event emitterSpecify subscription ownership and unsubscribe behavior
Implement LRU and LFU cachesExplain capacity, eviction, and retained values
Implement a JavaScript frame player controllerRelease scheduled work when playback ends

Choose a Frontend Engineer exercise, draw its ownership graph, and identify the edge that should disappear when the feature closes. Then describe the exact experiment that would show whether your repair worked.

Sources and Further Reading

Research checked September 14, 2026. Browser tooling and framework behavior should be verified against the versions used in your investigation.