How does garbage collection work?
By FrontendPro Editorial Team Updated 9/26/2026
Answer
JavaScript frees memory automatically. The engine decides what to free using reachability: anything still reachable from a set of roots is kept, everything else is collected.
Roots are the global object, the current call stack, and closures currently in scope. From there the collector walks every reference.
Mark and sweep
- Mark — traverse the object graph from the roots, marking everything reached.
- Sweep — free whatever was not marked.
This is why reference counting was abandoned: two objects pointing at each other keep a non-zero count forever, but if nothing outside reaches them, mark-and-sweep collects the pair correctly.
Generational collection
Most objects die young, so V8 splits the heap. New objects go into a small young generation scavenged frequently and cheaply; survivors are promoted to the old generation, collected less often with a fuller pass. Modern engines do much of this incrementally and on background threads to avoid long pauses.
You cannot control it
There is no free(), and delete removes a property, not an object. Setting obj = null only drops your reference — the object goes when the last reference does.
Where leaks actually come from
Memory is not leaked by the collector failing; it is leaked by references you forgot:
- Listeners and timers never removed, especially on
documentorwindow, which outlive the component that added them. UseAbortControlleror clean up on unmount. - A closure holding a large object. A closure keeps its whole scope alive, not just the variables it uses.
- Detached DOM nodes. An element removed from the page but still referenced by an array or a variable cannot be collected, and it keeps its subtree with it.
- Caches that only grow. Use
WeakMaporWeakSetwhen the entry should disappear once the key does.
Find them in the Memory panel: take a heap snapshot, act, snapshot again, and compare what stayed.
