Explain how JavaScript garbage collection works at a high level, why memory pressure can cause UI jank (GC pauses), and how to investigate it with Chrome DevTools (Performance + Memory panels). Include WeakMap/WeakRef use cases and common pitfalls.
Garbage Collection (GC) in JavaScript: Memory Pressure and Jank
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Big idea
JavaScript is garbage-collected: you don’t free memory manually. But you do control which objects remain reachable. If your app allocates lots of short-lived objects or accidentally retains references, the GC has more work to do — and that can show up as jank (stutters) in the UI.
Concept | What it means | Why you care (frontend) |
|---|---|---|
Reachability | If something is reachable from roots (global, closures, DOM refs), GC won't collect it | Leaks are usually "still referenced", not "GC failed" |
Memory pressure | Heap grows → GC runs more often / more aggressively | Frequent GCs can compete with rendering |
GC pauses | Some GC work stops the world briefly | Pauses can push frames over 16ms and hurt INP |
Practical scenario
You render a feed and create new objects on every scroll (formatting strings, building derived arrays, cloning objects). The UI looks fine on a dev machine, but on mid-range devices you see stutters. Often the hidden cause is allocation churn: the GC runs repeatedly, causing small pauses that add up.
// Allocation-heavy pattern (creates new arrays/objects frequently)
function render(items) {
const rows = items
.filter(x => x.visible)
.map(x => ({ id: x.id, label: `${x.name} (${x.count})` }));
// ... render rows
}
// Lower-churn patterns:
// - avoid repeated cloning
// - memoize derived data
// - keep caches bounded (LRU)
// - reuse arrays where possible (carefully)
How to investigate in Chrome DevTools
1) Performance panel: record a trace while reproducing jank.
- Look for long tasks, layout thrash, and also GC activity markers.
2) Memory panel:
- Take heap snapshots before/after repeating an interaction.
- Use Retainers to see what keeps objects alive.
- Use Allocation instrumentation to find hot allocation sites.
If memory keeps growing and never comes down after a "steady-state" interaction, you likely have a retention leak.
Common leak source | Why it retains | Fix pattern |
|---|---|---|
Event listeners / subscriptions | Callback closures keep data reachable | Remove/unsubscribe on cleanup |
Unbounded caches (Map, arrays) | No eviction → heap grows forever | Bounded cache (LRU/TTL) + key hygiene |
Detached DOM nodes | JS references keep removed nodes alive | Don't store DOM nodes long-term; null references |
Timers | Intervals keep closures alive | clearInterval/clearTimeout in cleanup |
WeakMap / WeakRef: when they help
WeakMapkeys are weakly held: if the key object becomes unreachable, the entry can be collected.- This is useful for memoization keyed by objects (DOM nodes, component instances) where you don't want your cache to keep them alive.
They are not a silver bullet: you still need to remove event listeners, bound caches, and avoid retaining large graphs in closures.
Pitfalls
- Treating GC as a fix: "it will clean up" → not if you still reference it.
- Allocating aggressively in hot paths (scroll, animation frames).
- Using caches without eviction or with high-cardinality keys.
- Profiling only in dev builds (prod code can behave differently).
Answer to land
"If the UI is janky after repeated interactions, I profile both CPU and memory. I look for allocation churn and GC markers in the Performance panel, then use heap snapshots and retainers to confirm what stays reachable. Fixes are usually: cleanup subscriptions/listeners, bound caches (LRU/TTL), and reduce allocations in hot paths."