Browser Garbage Collection: What Frontend Engineers Should Know
Why your app’s memory leaks, and how browsers quietly sweep up your JavaScript and DOM clutter
August 10, 2026

Ever had your web app slow down after a while or start feeling like it’s chewing through more and more memory? You might have suspected memory leaks , but where exactly do they come from in a browser? How does the browser even decide what memory to free or keep?
I hit this problem recently debugging a single-page app that got sluggish after a few minutes. Some tabs just ballooned their memory usage, and Chrome’s Task Manager was confirming my suspicion: memory just kept climbing.
Turns out, understanding how browsers do garbage collection under the hood cleared up a lot of mystery. It’s not just about your JavaScript variables. The DOM, event listeners, and even closures can silently hold onto memory long after you think you’re done with them.
Let’s walk through what’s really going on.
Garbage collection basics: the browser’s janitor
Your browser’s JavaScript engine, like V8 in Chrome or SpiderMonkey in Firefox, runs your scripts and manages memory. Garbage collection (GC) is the process that finds objects in memory that are no longer reachable by your running code and frees that memory so the app doesn’t grow endlessly.
But how does the browser know what’s "no longer reachable"? It uses a concept called reachability. If your code or the system can still access a value through some chain of references, it’s considered live. Anything else is garbage and eligible to be collected.
Imagine a graph where nodes are objects and edges are references. GC algorithms like mark-and-sweep start from root objects (globals, stack variables) and mark everything reachable. Then everything unmarked is swept away.
But things get tricky when you add the DOM and event listeners into the mix.
JavaScript objects and closures: Your typical GC suspects
When you create variables, objects, functions, or closures, they live in memory as long as you keep references to them. For example:
function makeCounter() {
let count = 0;
return () => ++count;
}
const counter = makeCounter();
counter();
Here, the closure returned by makeCounter keeps a hidden reference to count. Garbage collection won’t free count as long as the counter function exists.
This is straightforward, but leaks happen when you accidentally keep references alive longer than needed. For example, storing large objects in global variables or arrays you never clear.
The DOM’s role in memory management
Unlike plain JavaScript objects, DOM nodes are managed both by the browser’s rendering engine and the JavaScript engine. When you create elements:
const div = document.createElement('div');
document.body.appendChild(div);
The browser creates a DOM tree node for that element. But what happens when you remove it?
document.body.removeChild(div);
You might think the node is gone, but if you still hold a JavaScript reference to div, the browser cannot reclaim that memory. That node , and all its children and event listeners , remain in memory.
This is a classic source of leaks: detached DOM nodes.
Event listeners: The sneaky retainers
Event listeners can silently keep DOM nodes and other objects alive. Consider this:
const button = document.querySelector('button');
function onClick() {
console.log('Clicked');
}
button.addEventListener('click', onClick);
// Later
button.remove();
If you remove button from the DOM but never call button.removeEventListener('click', onClick), the event listener reference stays registered. Many browsers keep that listener and the node alive because the event system holds a reference to the callback and the node.
This leads to memory leaks from unremoved event listeners.
Common memory leak patterns to watch out for
-
Detached DOM nodes: You remove elements from the document but keep references to them in JavaScript variables or closures.
-
Forgotten event listeners: Adding listeners without removing them when elements or components unmount.
-
Closures over large objects: Closures accidentally capturing large data structures that are no longer needed.
-
Timers and intervals: Using
setIntervalorsetTimeoutwithout clearing them when components unmount, keeping references alive. -
Caches that grow indefinitely: Storing data in arrays or maps without eviction policies.
How browsers perform garbage collection under the hood
Modern JavaScript engines use generational garbage collection. They optimize for the fact that most objects die young.
- Young generation: Newly created objects are allocated here. GC runs frequently and quickly here.
- Old generation: Objects that survive multiple GC cycles are promoted here. GC runs less frequently but is more thorough.
This means your memory leaks often occur in the old generation , objects that never get freed because something still holds a reference.
When a GC cycle runs, it starts from root objects like:
- Global window and document objects
- Active function call stacks
- Registered event listeners
and marks all reachable objects.
Anything not reachable is cleaned up, including detached DOM nodes and unused JavaScript objects.
Debugging memory leaks: Practical tips
Chrome DevTools Memory tab
Open Chrome DevTools and go to the Memory panel.
- Heap snapshot: Take snapshots before and after interactions to see what objects remain.
- Allocation instrumentation: Record allocations over time to find growing memory.
- Allocation sampling: Profile allocations to find high-memory objects.
Detect detached DOM nodes
In the Elements panel, you can search for nodes that are no longer attached to the document but still in memory.
You can also use this snippet to find detached nodes:
const all = [...document.querySelectorAll('*')];
const detached = all.filter(el => !document.contains(el));
console.log(detached);
Track event listeners
Chrome DevTools also lets you inspect event listeners on DOM nodes. Make sure listeners are removed when not needed.
Use getEventListeners(node) in the console to list them.
Heap snapshot comparisons
Take snapshots at different times and compare to see what objects persist unexpectedly.
Profiling closures
Closures holding memory can be tricky. Look for retained objects in heap snapshots and trace retainers.
Practical habits to avoid leaks
- Always clean up event listeners when elements or components unmount.
- Remove references to DOM nodes after removing them from the document.
- Clear timers and intervals when no longer needed.
- Avoid storing large data in global variables or long-lived caches without limits.
- Use tools like Chrome DevTools regularly to profile memory, especially after complex UI interactions or dynamic content loads.
When frameworks help , and when they don’t
Modern frontend frameworks like React, Vue, and Angular help manage DOM lifecycle and event listeners for you. But leaks still happen:
- If you create event listeners outside the framework lifecycle.
- If you keep references to DOM nodes in variables or closures.
- If you misuse refs or fail to clean up timers.
Understanding browser GC mechanics helps you spot these issues even when using frameworks.
Next time your web app feels sluggish or your tab’s memory ballooning, remember it’s usually a matter of references the browser can’t drop. The garbage collector is silently cleaning up what it can, but it can’t free what you still hold.
Knowing how JavaScript objects, DOM nodes, and event listeners interact with memory lets you write cleaner code, prevent leaks, and keep your app running smoothly , no magic, just a bit of detective work under the hood.