One of the most dangerous myths in frontend development is: "JavaScript has a Garbage Collector, so I don't need to manage memory."
This is the kind of thinking that leads to Single Page Applications (SPAs) that start snappy but become sluggish and unresponsive after 45 minutes of usage.
Today, we are going to look at the invisible tether that keeps dead objects alive: The Closure.
The Mental Model: The Reachability Graph
To understand leaks, you need to understand how JavaScript deletes things. It uses an algorithm called Mark-and-Sweep.
Imagine your application is a series of islands connected by bridges.
-
The Main Island: This is the
window(root). - The Bridges: These are references (variables, properties).
Periodically, the Garbage Collector (GC) starts at the Main Island and crosses every bridge it can find. If it can reach an island, it keeps it. If an island has no bridges connecting it to the Main Island, the GC assumes it is lost at sea and destroys it to free up space.
A Memory Leak happens when you think you destroyed a bridge, but an invisible rope (a Closure) is still holding onto the island.
The "Shallow" Mistake: The Accidental Anchor
Let's look at a classic React/Vanilla JS trap. We create a dashboard widget that listens for window resize events.
// ❌ The Memory Leak
function attachWidget() {
const massiveData = new Array(100000).fill('Heavy Data'); // 10MB Object
function handleResize() {
// This closure "captures" massiveData
console.log('Widget Resized', massiveData.length);
}
window.addEventListener('resize', handleResize);
}
// We mount the widget
attachWidget();
// Later, we "destroy" the widget... or so we think.
// The user navigates away, removing the UI from the DOM.
Why this leaks
You might think: "The function finished running, so massiveData should be cleared, right?"
No.
-
window(The Root) has a reference tohandleResize(via the Event Listener). -
handleResizeis a Closure. It captures the scope it was created in. - Therefore,
handleResizeholds a strong reference tomassiveData.
Even though the user can't see the widget anymore, massiveData is still sitting in RAM. The GC cannot touch it because window is still holding the rope. Do this 10 times, and your app crashes.
The "Deep" Fix: Severing the Tether
A Senior Engineer knows that every addEventListener is a potential memory leak. You must manually cut the rope.
// ✅ The Fix
function attachWidget() {
const massiveData = new Array(100000).fill('Heavy Data');
function handleResize() {
console.log('Widget Resized', massiveData.length);
}
window.addEventListener('resize', handleResize);
// Return a cleanup function (The "Unsubscribe" pattern)
return function cleanup() {
window.removeEventListener('resize', handleResize);
};
}
When you call removeEventListener, you cut the bridge from window to handleResize. Now that handleResize is unreachable, the GC sweeps it away, and massiveData falls into the ocean with it.
Conclusion
Closures are powerful because they remember data. But that is also why they are dangerous.
The Golden Rule: If a closure reads a large variable, that closure keeps that variable alive forever—or at least until the closure itself is destroyed.
Always ask yourself: "Who is holding onto this function?" If the answer is window, document, or setInterval, you have a leak waiting to happen.
Top comments (0)