JavaScript - The Garbage Collector is Your Roommate (and you are a bad tenant)
You are not the landlord of this memory. You are just a tenant. And the cleaning service, V8’s Garbage Collector (GC), doesn’t come on your command. It comes when it feels like it.
Let’s start with a line of code I see in Pull Requests from developers who think they are “optimizing” memory usage.
const user = {
id: 1,
hugeData: new Array(1000000).fill('garbage')
};
// "I'm freeing up RAM, boss!"
delete user.hugeData;You think you just freed 8MB of RAM. You think the memory was released the moment that semicolon hit.
But, in reality, you didn’t free memory.
You just deleted a key from a hash map (and destroyed the Hidden Class optimization in the process, but that’s a rant for another day).
The massive array still exists in the Heap. It sits there, occupying space, waiting.
You are not the landlord of this memory. You are just a tenant. And the cleaning service, V8’s Garbage Collector (GC), doesn’t come on your command. It comes when it feels like it.
Mark and Sweep
Think of your application’s memory (the Heap) as a messy apartment.
You are the hoarders.
The Garbage Collector is your obsessive-compulsive roommate who cleans up after you.
V8 uses an algorithm primarily based on Mark and Sweep. Here is how your roommate decides what stays and what goes
The Roots (Entry Points)
The roommate starts at the front door. In V8 terms, these are the Roots.
The global object (
windoworglobal).The current Call Stack (local variables in executing functions).
Reachability (The “Touch” Rule)
The roommate walks through the house touching everything they can reach.
They touch the Root (Global).
The Global object references a
Userobject? They walk to theUserobject and touch it.The
Userobject references aProfile? They touch theProfile.
This is the concept of Reachability.
If there is a chain of references (a path) from the Root to an object, that object is “Reachable.” It is marked as “Alive.”
The Sweep (The Trash)
Once the roommate has traced every single path and marked every reachable item, they look at what’s left.
That massive array you created inside a function that returned 5 minutes ago? No one is holding it.
That object you set to
null? The path is broken.
The roommate sweeps everything unmarked into the trash. Only now is the memory actually freed.
Generational Collection
If the roommate checked every single item in the house every time they cleaned, your app would be agonizingly slow.
To solve this, V8 divides the Heap into two neighborhoods, The Young Generation (Nursery) and the Old Generation.
Young Generation
This is where all new objects are born. It is small (usually 1 to 8 MB).
High turnover. Most objects die young (temporary variables, loop iterators).
The GC runs a “Scavenge” (Minor GC) here very frequently. It is fast because most things here are dead anyway. It flips the survivors into a clean space and wipes the rest.
The Old Generation
If an object survives two cycles of Scavenging in the Nursery, the GC says, “Okay, this thing is sticking around.” It gets promoted (moved) to the Old Generation.
This is the attic. It’s huge (can be gigabytes).
Cleanup here is expensive.
Stop-the-World Pauses
Here is the nightmare scenario.
You keep promoting garbage to the Old Generation. The attic gets full.
When the Old Generation is saturated, the Scavenger can’t handle it. V8 has to call in the heavy machinery: The Major GC (Mark-Compact).
To do this, V8 initiates a Stop-the-World pause.
It freezes your application. No click events. No server requests. No rendering.
The engine pauses execution to traverse the entire massive Old Heap, mark the living, and compact the memory to remove fragmentation.
If your user sees the UI stutter or your server drops a connection, it’s often because your roommate paused the world to clean up your mess.
The “Bad Tenant” Behavior
How do you saturate the Old Generation and cause these stutters? By being an unintentional hoarder.
The Forgotten Timer
function startTracking() {
const heavyObject = new Array(1000000); // 8MB
setInterval(() => {
// This callback references heavyObject
console.log(heavyObject.length);
}, 1000);
}You ran startTracking() once. You forgot about it.
The setInterval references the callback. The callback references heavyObject (closure).
The result, a heavyObject is reachable from the Root (via the Timer). It survives the Nursery. It moves to Old Gen. It never dies.
The Detached DOM Node
You have a modal dialog.
let myModal = document.getElementById('modal');
document.body.removeChild(myModal);You removed it from the DOM. The user can’t see it. The “Sweep” should pick it up, right?
Wrong.
The variable myModal still holds a reference to that DOM node in JavaScript memory. Even worse, if that DOM node has pointers to its children, you are keeping the entire tree alive in memory.
It is a “Detached Node”, a ghost haunting your heap.
So what to do?
Stop treating RAM like it’s infinite.
Use WeakMap and WeakRef
If you need to associate data with an object but don’t want that association to keep the object alive, use a WeakMap.
let user = { name: "Alice" };
const cache = new WeakMap();
cache.set(user, "Some Metadata");
user = null; // The reference is broken.In a normal Map, cache would keep “Alice” alive.
In a WeakMap, the GC ignores the reference inside the map.
When user becomes unreachable elsewhere, the WeakMap entry is auto-deleted.
Inspect the Snapshots
Stop guessing. Open Chrome DevTools → Memory tab.
Take a Heap Snapshot.
Run your action (open/close the modal).
Force a GC (click the trash can icon).
Take a second Snapshot.
Compare.
If the Delta is positive and growing every time you repeat the action, you are leaking.
You are the bad tenant.
Clean up your room.


