Layout Thrashing & Reflow
advancedWhy interleaving DOM reads and writes forces synchronous layout, the read/write batching fix, requestAnimationFrame, and compositor-only properties.
How JavaScript garbage collection actually works — reachability, mark-and-sweep, generational GC — the four classic memory leaks, and how WeakMap, WeakRef, and heap snapshots let you fix them.
JavaScript has automatic garbage collection, so you can't leak memory. True or false — and if false, show me a leak.
It's false, and the interviewer knows it. Automatic GC frees memory that's unreachable; a leak in JS is memory you're still reaching by accident — a reference you forgot to drop. The skill isn't managing memory by hand, it's recognizing the reference graphs that quietly grow forever.
The one-liner: GC collects the unreachable; a leak is unwanted reachability.
Reachability, not reference counting. The collector starts from roots (the global object, the current call stack, active closures) and marks everything reachable from them. Anything unmarked is garbage and gets swept. This is why cycles are not leaks: two objects that point at each other but are unreachable from any root are both collected. (Naive reference counting can't do that — it's why the DOM/JS boundary leaked in old IE.)
Generational, in V8. Most objects die young, so the heap is split:
You can't force it. There's no portable gc() in production code — you influence collection by dropping references so objects become unreachable. delete obj.key, arr.length = 0, or reassigning a variable to null all sever edges in the graph.
function process() {
leaked = new Array(1_000_000); // no var/let/const → global, never collected
}An unqualified assignment (outside strict mode) creates a property on the global object — a permanent root. "use strict" turns this into a ReferenceError, which is why it's on by default in modules.
function startPolling(node) {
const bigState = buildHugeState();
setInterval(() => {
node.textContent = read(bigState); // closure keeps node AND bigState alive forever
}, 1000);
}The interval callback is reachable from the timer system (a root) for as long as the interval lives, and it closes over node and bigState. Nothing here is ever collected. Fix: keep the timer id and clearInterval it; removeEventListener on teardown. In React, that's the effect cleanup return.
const cache = {};
function cacheRow(id) {
cache[id] = document.getElementById(id); // JS reference to the node
}
// later:
container.innerHTML = ""; // removed from the document...
// ...but cache[id] still points at it → detached, alive, leakedRemoving a node from the document doesn't free it if JavaScript still holds a reference. The whole subtree stays in memory. This is the single most common SPA leak, and heap snapshots label these nodes "Detached."
A Map used as a cache that only ever sets is a leak with a friendly name. So is a closure that captures a large object it doesn't need. memoize and API-call caching both flag this: without eviction, the cache is the leak. The fix is bounded eviction (LRU) or weak keys.
// A
el.addEventListener("click", handler);
el.remove();
// B
const wm = new WeakMap();
wm.set(el, meta);
el.remove();WeakMap, WeakSet, WeakRefWeakMap / WeakSet hold their keys weakly: an entry doesn't count as a reference for reachability, so keying metadata by a DOM node or object lets both disappear the moment the rest of your code stops using them. That's why they're the right structure for "extra data attached to an object I don't own the lifecycle of." They're not enumerable (you can't iterate them) precisely because entries can vanish at any GC.
const metadata = new WeakMap();
function tag(node, info) {
metadata.set(node, info); // when node is GC'd, this entry goes with it
}WeakRef + FinalizationRegistry are the escape hatch for caches that want to observe collection — hold a value weakly and get a callback when it's reclaimed. Treat them as a last resort: the spec makes no guarantees about when (or whether) finalizers run, so never put required cleanup in a FinalizationRegistry. Interviewers like hearing that caveat more than the API.
You find leaks the same way you find slow code — by measuring, not guessing:
performance.measureUserAgentSpecificMemory() — an async, cross-origin-isolated API for total tab memory in the field. (The old performance.memory is non-standard and coarse.)--expose-gc exists only for debugging.Why interleaving DOM reads and writes forces synchronous layout, the read/write batching fix, requestAnimationFrame, and compositor-only properties.
Big-O reasoning the way interviewers want it: the Map/Set/object/array cheat sheet, hidden O(n) costs like spread and includes, and trading memory for speed.
A hands-on exercise: profile first, fix the algorithm, hoist invariant work, cut allocations, memoize, then chunk off the main thread — in that order.
performance.now vs Date.now, the User Timing API, PerformanceObserver, DevTools flame charts, and the micro-benchmark traps that make numbers lie.