π¦ Memory Lifecycle in JavaScript
JavaScript automatically manages memory, but understanding the lifecycle helps you avoid memory leaks.
What Gets Allocated?
// Primitives - stored on stack (fast, auto-managed)
var number = 42;
var string = "hello";
var boolean = true;
// Objects - stored on heap (need garbage collection)
var obj = { name: "Sandeep" };
var arr = [1, 2, 3];
var fn = function() { };
var date = new Date();
// Each object allocation uses heap memory
// Heap is where GC does its work
ποΈ Garbage Collection
JavaScript uses Mark and Sweep algorithm to find and free unused memory.
Mark and Sweep Algorithm
What Are "Roots"?
// These are the starting points for GC's reachability check:
// 1. Global object (window in browser, global in Node)
window.myGlobal = { data: "accessible" };
// 2. Currently executing function's variables
function running() {
var local = { data: "accessible while function runs" };
// local is reachable here
} // After function ends, local becomes unreachable
// 3. Closure references
function outer() {
var closureVar = { data: "kept alive by closure" };
return function() {
console.log(closureVar); // closureVar is reachable!
};
}
When Does GC Run?
- Memory pressure (running low)
- CPU idle time
- Internal heuristics
You cannot force GC in normal code. You can only make objects eligible for collection by removing references.
Reachable vs Eligible for GC
var user = { name: "Sandeep" };
// user object is REACHABLE (variable points to it)
// GC will NOT collect it
user = null;
// Now the object is UNREACHABLE (no references)
// Object is ELIGIBLE for GC
// But GC hasn't run yet - object still in memory!
// ... time passes ...
// GC runs when engine decides
// Object finally freed from memory
π Common Memory Leaks
A memory leak happens when objects that should be garbage collected are kept alive by unintended references.
1. Accidental Global Variables
β The Leak
function createLeak() {
leak = "I'm global now!"; // Forgot var/let/const!
// Attaches to window object, never cleaned up
}
function anotherLeak() {
this.data = "Also global!"; // In non-strict mode, this = window
}
β The Fix
"use strict"; // Prevents accidental globals
function noLeak() {
const local = "I'm local and will be GC'd";
}
// Or always use var/let/const
function safe() {
let data = "Properly scoped";
}
2. Forgotten Event Listeners
β The Leak
class Component {
constructor() {
this.data = new Array(1000000).fill('x'); // Large data
// Listener keeps 'this' (and this.data) alive!
window.addEventListener('resize', this.handleResize);
}
handleResize = () => {
console.log(this.data.length);
}
// Component removed from DOM...
// But listener still exists on window!
// this.data can NEVER be garbage collected!
}
β The Fix
class Component {
constructor() {
this.data = new Array(1000000).fill('x');
this.boundHandler = this.handleResize.bind(this);
window.addEventListener('resize', this.boundHandler);
}
handleResize() {
console.log(this.data.length);
}
// Call this when component is removed!
destroy() {
window.removeEventListener('resize', this.boundHandler);
this.boundHandler = null;
}
}
3. Closures Holding Large Data
β The Leak
function createHandler() {
// Large data - should be temporary
const largeData = new Array(1000000).fill('x');
// Process it
const result = largeData.length;
// Return handler that DOESN'T use largeData
return function handler() {
console.log('Result:', result);
};
// But largeData might still be in the closure!
// Different JS engines handle this differently
}
β The Fix
function createHandler() {
let largeData = new Array(1000000).fill('x');
const result = largeData.length;
largeData = null; // Explicitly release reference!
return function handler() {
console.log('Result:', result);
};
// Now largeData can be garbage collected
}
4. Forgotten Timers
β The Leak
function startPolling() {
const data = { /* large object */ };
setInterval(() => {
fetch('/api/data').then(res => {
Object.assign(data, res);
});
}, 1000);
// Interval runs FOREVER
// Keeps 'data' alive forever
// Even if you navigate away (in SPA)
}
β The Fix
function startPolling() {
const data = { /* large object */ };
const intervalId = setInterval(() => {
fetch('/api/data').then(res => {
Object.assign(data, res);
});
}, 1000);
// Return cleanup function
return function stopPolling() {
clearInterval(intervalId);
};
}
const stop = startPolling();
// Later: stop();
5. Detached DOM Nodes
β The Leak
const elements = [];
function addElement() {
const div = document.createElement('div');
div.innerHTML = '<img src="large-image.jpg">';
document.body.appendChild(div);
elements.push(div); // Store reference
}
function removeElements() {
elements.forEach(el => el.remove());
// DOM nodes removed from page...
// But 'elements' array still holds them!
// They're "detached" but not garbage collected
}
β The Fix
const elements = [];
function addElement() {
const div = document.createElement('div');
div.innerHTML = '<img src="large-image.jpg">';
document.body.appendChild(div);
elements.push(div);
}
function removeElements() {
elements.forEach(el => el.remove());
elements.length = 0; // Clear the array too!
// Or: elements = [];
}
6. Growing Cache Without Limit
β The Leak
const cache = {};
function memoize(fn) {
return function(...args) {
const key = JSON.stringify(args);
if (!(key in cache)) {
cache[key] = fn(...args);
}
return cache[key];
};
}
// Cache grows forever!
// In a long-running app, memory keeps increasing
β The Fix: LRU Cache with Max Size
function memoizeWithLimit(fn, maxSize = 100) {
const cache = new Map(); // Map preserves insertion order
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
const value = cache.get(key);
cache.delete(key);
cache.set(key, value); // Move to end (most recent)
return value;
}
const result = fn(...args);
cache.set(key, result);
// Remove oldest if over limit
if (cache.size > maxSize) {
const oldest = cache.keys().next().value;
cache.delete(oldest);
}
return result;
};
}
π WeakMap & WeakSet
Special collections that hold weak references - they don't prevent garbage collection of their keys/values.
Strong vs Weak References
| Reference Type | Prevents GC? | Examples |
|---|---|---|
| Strong | β Yes | Variables, Array items, Map keys, Object properties |
| Weak | β No | WeakMap keys, WeakSet values |
How WeakMap Works
WeakMap vs Map
| Feature | Map | WeakMap |
|---|---|---|
| Key types | Any (string, number, object) | Objects ONLY |
| Iterable | β Yes (.keys(), .values(), .forEach()) | β No |
| .size property | β Yes | β No |
| Keys prevent GC | β Yes (strong reference) | β No (weak reference) |
| Predictable | β Data stays until you remove | β Data can vanish after GC |
WeakMap Example
// Regular Map - PREVENTS garbage collection
const cache = new Map();
let user = { name: "Sandeep" };
cache.set(user, "metadata");
user = null; // Clear our reference
// BUT the Map still holds the user object as a key!
// Object can NOT be garbage collected
console.log(cache.size); // 1 (still there!)
// WeakMap - ALLOWS garbage collection
const cache = new WeakMap();
let user = { name: "Sandeep" };
cache.set(user, "metadata");
user = null; // Clear our reference
// No strong references remain!
// Object CAN be garbage collected
// WeakMap entry automatically removed
// (can't check .size - WeakMap doesn't have it!)
WeakSet
Same concept as WeakMap, but for storing objects (not key-value pairs).
const visited = new WeakSet();
function trackVisit(element) {
if (visited.has(element)) {
console.log("Already visited!");
return;
}
visited.add(element);
// Process element...
}
// When element is removed from DOM and GC'd,
// it's automatically removed from WeakSet too!
WeakMap/WeakSet can't tell you their size or let you iterate because entries can disappear at any time when GC runs. The collection doesn't know when that happens!
πΊοΈ Map vs WeakMap: When to Use Which
Use Map When...
Keys Are Primitives
// WeakMap CAN'T do this!
const prices = new Map();
prices.set('apple', 1.50);
prices.set('banana', 0.75);
Need to Iterate
// WeakMap CAN'T do this!
const users = new Map();
users.forEach((data, user) => {
console.log(user.name);
});
Need Count/Size
// WeakMap CAN'T do this!
const online = new Map();
console.log(`${online.size} users`);
Data Must Persist
// Shopping cart - can't disappear!
const cart = new Map();
cart.set('SKU123', { qty: 2 });
// Must survive until checkout
Use WeakMap When...
Attaching Metadata to Objects
// Metadata goes when object goes
const metadata = new WeakMap();
metadata.set(domElement, {
clicks: 0
});
Private Data for Objects
const privateData = new WeakMap();
class User {
constructor(password) {
privateData.set(this, { password });
}
}
Cache for Object Arguments
const computed = new WeakMap();
function process(obj) {
if (!computed.has(obj)) {
computed.set(obj, calc(obj));
}
return computed.get(obj);
}
Third-Party Library Data
// Don't prevent user's objects from GC
class TooltipLib {
tooltips = new WeakMap();
add(el, text) {
this.tooltips.set(el, text);
}
}
Real-World Scenario: Tooltip Library
β With Map: Your Library Can Cause Memory Leaks
class TooltipLibrary {
constructor() {
this.tooltips = new Map();
}
addTooltip(element, text) {
this.tooltips.set(element, { text });
}
// User MUST call this when removing elements!
removeTooltip(element) {
this.tooltips.delete(element);
}
}
// User's code:
const lib = new TooltipLibrary();
const button = document.createElement('button');
lib.addTooltip(button, 'Click me!');
button.remove();
// User forgot to call lib.removeTooltip(button)!
// Memory leak - button stuck in YOUR Map forever!
β With WeakMap: Automatic Cleanup
class TooltipLibrary {
constructor() {
this.tooltips = new WeakMap();
}
addTooltip(element, text) {
this.tooltips.set(element, { text });
}
// No removeTooltip needed!
}
// User's code:
const lib = new TooltipLibrary();
const button = document.createElement('button');
lib.addTooltip(button, 'Click me!');
button.remove();
// That's it! When button is GC'd, tooltip data goes too.
// YOUR library cannot cause memory leaks!
Decision Guide
- Map = "I own this data and control its lifecycle"
- WeakMap = "I'm just attaching info to someone else's objects"
π§ DevTools Memory Debugging
Chrome DevTools provides powerful tools to find and fix memory leaks.
Opening Memory Tab
Memory Tab Options
| Option | Use Case | What It Shows |
|---|---|---|
| Heap Snapshot | Most common - find what's in memory | All objects at a specific moment |
| Allocation Timeline | See allocations over time | Blue bars (allocated), grey (freed) |
| Allocation Sampling | Lower overhead profiling | Which functions allocate memory |
Taking Heap Snapshots
Understanding Snapshot Columns
| Column | Meaning |
|---|---|
| Constructor | Type of object (Array, Object, string, etc.) |
| # New | Objects created since previous snapshot |
| # Deleted | Objects removed since previous snapshot |
| # Delta | Net change (New - Deleted) |
| Alloc. Size | Memory allocated for new objects |
| Freed Size | Memory freed by deleted objects |
| Size Delta | Net memory change (+ = growing, - = freed) |
Finding Memory Leaks
Using Filters
In the "Filter by class" box, type:
| Filter | What It Shows |
|---|---|
Detached | DOM nodes removed from page but still in memory (common leak!) |
Array | All Array objects |
Object | Plain JavaScript objects |
Closure | Closure objects holding references |
(YourClassName) | Instances of your custom classes |
Understanding Retainers
When you click an object, the Retainers panel shows WHY it can't be garbage collected:
- Yellow = Directly referenced by JavaScript code
- Red = Detached DOM nodes (potential memory leak!)
Statistics View
Click on a Snapshot, then select Statistics tab at the bottom to see a pie chart of memory usage by type:
Pro Tips
- Force GC before snapshots - Click ποΈ icon to get accurate readings
- Take 3 snapshots - Baseline β After action β After GC to identify real leaks
- Right-click β "Reveal in Summary view" to see object details
- Look for "Detached" in filter to find DOM node leaks quickly
- Repeat actions multiple times (e.g., open modal 10 times) to make leaks more visible
π Summary
Memory Management Quick Reference
| Topic | Key Takeaway |
|---|---|
| Memory Lifecycle | Allocate (automatic) β Use β Release (GC) |
| Garbage Collection | Mark & Sweep - unreachable objects are freed |
| GC Timing | Automatic, not immediate - you can't force it in code |
| Memory Leaks | Objects kept alive by unintended references |
| Common Leaks | Globals, event listeners, timers, closures, detached DOM |
| Strong Reference | Prevents GC (Map, Array, Object, variables) |
| Weak Reference | Allows GC (WeakMap keys, WeakSet values) |
| Map | Any keys, iterable, persistent - you control lifecycle |
| WeakMap | Object keys only, not iterable - auto cleanup |
When to Use What
| Scenario | Use |
|---|---|
| Data that must persist (cart, settings) | Map |
| Need to iterate or count entries | Map |
| Primitive keys (strings, numbers) | Map |
| Metadata for objects you don't own | WeakMap |
| Private class data | WeakMap |
| Cache that shouldn't prevent GC | WeakMap |
| DOM element tracking | WeakMap/WeakSet |
Memory Leak Prevention Checklist
- β
Use
"use strict"to prevent accidental globals - β Remove event listeners in cleanup/destroy methods
- β Clear intervals/timeouts when done
- β Null out large data in closures when no longer needed
- β Clear arrays holding DOM references when removing elements
- β Use WeakMap for object metadata that should be auto-cleaned
- β Implement cache size limits (LRU pattern)
- β Use DevTools Memory tab to verify cleanup