Memory Management In Javascript. Part II - Memory leak scenarios


In the previous post, we understood how garbage collection (GC) works under the hood in JavaScript. Now, we're going to understand memory leak patterns. Here's an expanded explanation of each common memory leak pattern and how to prevent them.
1. Long-Lived Object Graphs or Global Variables
Problem: Variables declared in the global scope or objects referenced from global variables persist for the entire application lifetime. If these accumulate data over time, they can consume increasing amounts of memory.
Fix:
Use block-scoped variables (
let
,const
) instead ofvar
Avoid attaching data to global objects (
window
,global
)Explicitly nullify references when done with large objects
// Bad - global variable
var largeData = fetchHugeDataset();
// Better - scoped and cleared
function processData() {
const largeData = fetchHugeDataset();
// ...use data...
largeData = null; // Clear reference when done
}
2. Missing Weak References
Problem: Strong references prevent objects from being garbage collected even when they're only needed conditionally.
Fix: Use WeakMap or WeakSet when you need a dictionary/set that doesn't prevent garbage collection of its keys.
// Regular Map keeps keys alive
const map = new Map();
map.set(element, data); // element can't be GC'd while in map
// WeakMap allows keys to be GC'd
const weakMap = new WeakMap();
weakMap.set(element, data); // element can be collected
3. Closure Leaks
Problem: Functions that close over variables retain those variables in memory even if the outer function has completed execution.
Fix: Be mindful of what variables are captured in closures, especially in long-lived functions.
function createHeavyClosure() {
const hugeArray = new Array(1000000).fill("data");
// Bad - closure keeps hugeArray alive
return function() {
console.log('This closure keeps hugeArray in memory');
};
// Better - avoid closing over large data if not needed
}
4. Detached DOM Nodes
Problem: When DOM elements are removed from the document but still referenced by JavaScript, they remain in memory.
Fix: Clear references to DOM elements when they're removed.
const elements = [];
function addElement() {
const div = document.createElement('div');
document.body.appendChild(div);
elements.push(div); // Keeps reference
}
function removeElements() {
elements.forEach(el => el.remove());
// elements array still holds references!
}
// Fix
function cleanElements() {
elements.length = 0; // Clear references
}
5. Timers
Problem: setInterval
or setTimeout
callbacks can keep objects alive longer than intended, especially in single-page applications where components unmount.
Fix: Always clear timers when they're no longer needed.
const timer = setInterval(() => {
updateData();
}, 1000);
// MUST clean up on unmouting
clearInterval(timer);
6. Orphaned Event Listeners
Problem: Event listeners attached to global objects (window, document) or parent elements can prevent garbage collection of their targets and scoped variables.
Fix: Always remove event listeners when they're no longer needed.
function setup() {
const handler = () => console.log('Resized');
window.addEventListener('resize', handler);
// Later...
window.removeEventListener('resize', handler);
}
7. Console Log Retention
Problem: Objects logged to the console may be retained in memory by developer tools, even in production.
console.log(largeDataset);
console.log('Dataset :', largeDataset);
8. Unclosed Connections
Problem: WebSockets, Server-Sent Events, or other persistent connections can keep objects alive and consume resources.
Fix: Implement proper cleanup when connections are no longer needed.
const socket = new WebSocket(url);
// Important for SPA route changes or component unmounts
function cleanup() {
socket.close();
}
9. String/Array Building
Problem: String concatenation (+=
) creates many temporary strings, wasting memory. Array joining grows memory efficiently and combines strings just once at the end, making it faster and leaner for large strings.
Fix: Use array joins for building large strings.
// Bad - creates multiple intermediate strings
let result = '';
for (let i = 0; i < 100000; i++) {
result += 'text';
}
// Good - more memory efficient
const parts = [];
for (let i = 0; i < 100000; i++) {
parts.push('text');
}
const result = parts.join('');
10. Object Pooling
Problem: Frequently creating and discarding objects can trigger excessive garbage collection. use object pooling pattern to create an object to use over app and after relase the object to reduce GC overhead
https://egghead.io/blog/object-pool-design-pattern
Now that we've covered common memory leak patterns, in the next part we'll dive into monitoring techniques—I'll show you how to identify memory bottlenecks using Chrome DevTools
Stay tuned, and feel free to follow me on:
Subscribe to my newsletter
Read articles from Meiti directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Meiti
Meiti
TS/JS Engineer