🧠 Memory Management

Garbage Collection, Memory Leaks, WeakMap/WeakSet, and DevTools Debugging

πŸ“¦ Memory Lifecycle in JavaScript

JavaScript automatically manages memory, but understanding the lifecycle helps you avoid memory leaks.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ MEMORY LIFECYCLE β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 1. ALLOCATE 2. USE 3. RELEASE β”‚ β”‚ ──────────── ───── ──────── β”‚ β”‚ β”‚ β”‚ var obj = {} β†’ obj.name = "x" β†’ GC frees memory β”‚ β”‚ var arr = [] arr.push(1) when unreachable β”‚ β”‚ function fn(){} fn() β”‚ β”‚ β”‚ β”‚ (Automatic) (Your code) (Automatic) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

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
Key Insight: You don't manually allocate or free memory in JavaScript. The engine handles allocation when you create things, and garbage collection handles cleanup. Your job is to not hold unnecessary references.

πŸ—‘οΈ Garbage Collection

JavaScript uses Mark and Sweep algorithm to find and free unused memory.

Mark and Sweep Algorithm

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ MARK & SWEEP ALGORITHM β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ ROOT (Global object, current call stack, closures) β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€β†’ obj1 ──→ obj2 βœ“ Reachable (KEEP) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └──→ obj3 βœ“ Reachable (KEEP) β”‚ β”‚ β”‚ β”‚ β”‚ └──→ obj4 βœ“ Reachable (KEEP) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ obj5 ──→ obj6 βœ— Unreachable (GARBAGE COLLECT!) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ GC Process: 1. Start from "roots" (global object, current execution context) 2. "Mark" all objects reachable from roots (follow all references) 3. "Sweep" (free) all unmarked objects

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?

GC is NOT immediate! The JavaScript engine decides when to run GC based on:
  • 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 TypePrevents GC?Examples
Strongβœ… YesVariables, Array items, Map keys, Object properties
Weak❌ NoWeakMap keys, WeakSet values

How WeakMap Works

BEFORE clearing userReference: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” userReference ───┼──► user object β”‚ (strong reference) β”‚ β”‚ weakMap ─ ─ ─ ─ ─┼─ ─► user object β”‚ (WEAK reference, doesn't count) β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Strong reference exists = object stays alive AFTER userReference = null: β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” userReference ───┼──► (null) β”‚ β”‚ β”‚ weakMap ─ ─ ─ ─ ─┼─ ─► user object β”‚ ← Weak ref doesn't prevent GC! β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ No strong references = object CAN be garbage collected! WeakMap entry automatically disappears!

WeakMap vs Map

FeatureMapWeakMap
Key typesAny (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!
Why No .size or Iteration?

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

Do you need primitive keys (string, number)? └── YES β†’ Use Map └── NO (objects only) ↓ Do you need to iterate, count, or list all entries? └── YES β†’ Use Map └── NO ↓ Should data persist even if object is unused elsewhere? └── YES β†’ Use Map (you control deletion) └── NO β†’ Use WeakMap (auto cleanup)
Simple Rule:
  • 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

1
Press F12 or Cmd+Option+I (Mac) / Ctrl+Shift+I (Windows)
2
Click Memory tab (may need to click >> to find it)

Memory Tab Options

OptionUse CaseWhat It Shows
Heap SnapshotMost common - find what's in memoryAll objects at a specific moment
Allocation TimelineSee allocations over timeBlue bars (allocated), grey (freed)
Allocation SamplingLower overhead profilingWhich functions allocate memory

Taking Heap Snapshots

1
Click the ⏺ record button or "Take snapshot"
2
Wait for snapshot to complete (may take a few seconds)
3
Explore using Summary, Comparison, Containment, or Statistics views

Understanding Snapshot Columns

ColumnMeaning
ConstructorType of object (Array, Object, string, etc.)
# NewObjects created since previous snapshot
# DeletedObjects removed since previous snapshot
# DeltaNet change (New - Deleted)
Alloc. SizeMemory allocated for new objects
Freed SizeMemory freed by deleted objects
Size DeltaNet memory change (+ = growing, - = freed)

Finding Memory Leaks

1
Take Snapshot #1 (baseline)
2
Perform the action you suspect leaks (e.g., open/close modal 5 times)
3
Click πŸ—‘οΈ garbage bin icon to force GC
4
Take Snapshot #2
5
In dropdown, select "Comparison" and compare to Snapshot 1
6
Look at # Delta column - positive numbers = potential leak

Using Filters

In the "Filter by class" box, type:

FilterWhat It Shows
DetachedDOM nodes removed from page but still in memory (common leak!)
ArrayAll Array objects
ObjectPlain JavaScript objects
ClosureClosure 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:

Your Object (what you're investigating) └── held by: Array @12345 └── held by: regularMap (Map) └── held by: Window (global) This chain shows you EXACTLY what's keeping the object alive! To fix the leak: break one of these references.
Yellow vs Red Objects in Retainers:
  • 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:

Statistics shows memory breakdown: Code β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 30% Strings β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 25% JS arrays β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 20% Typed arrays β–ˆβ–ˆβ–ˆβ–ˆ 10% System objects β–ˆβ–ˆβ–ˆ 8% Other β–ˆβ–ˆ 7% Useful for understanding WHAT is using memory.

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

TopicKey Takeaway
Memory LifecycleAllocate (automatic) β†’ Use β†’ Release (GC)
Garbage CollectionMark & Sweep - unreachable objects are freed
GC TimingAutomatic, not immediate - you can't force it in code
Memory LeaksObjects kept alive by unintended references
Common LeaksGlobals, event listeners, timers, closures, detached DOM
Strong ReferencePrevents GC (Map, Array, Object, variables)
Weak ReferenceAllows GC (WeakMap keys, WeakSet values)
MapAny keys, iterable, persistent - you control lifecycle
WeakMapObject keys only, not iterable - auto cleanup

When to Use What

ScenarioUse
Data that must persist (cart, settings)Map
Need to iterate or count entriesMap
Primitive keys (strings, numbers)Map
Metadata for objects you don't ownWeakMap
Private class dataWeakMap
Cache that shouldn't prevent GCWeakMap
DOM element trackingWeakMap/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