DocsHub
Advanced

Memory Management

Learn how JavaScript manages memory, how garbage collection works, and how to avoid memory leaks.

Memory Management

JavaScript manages memory automatically. You do not manually allocate or free memory like in C or C++. JavaScript handles it for you through a process called garbage collection.

But automatic memory management does not mean you never have to think about memory. Memory leaks — situations where memory is held longer than needed — are real, common, and can make your application slow or crash over time.

Understanding how JavaScript manages memory helps you write code that does not leak.


The Memory Lifecycle

Every piece of data in a JavaScript program goes through three stages:

AllocateMemory is reservedfor your data UseRead and writethe data ReleaseMemory is freedwhen no longer needed

JavaScript handles allocation and release automatically. You only deal with the middle part — using the data.

// Allocation — memory is reserved
const name = "Ali";              // string allocated
const user = { name: "Ali" };    // object allocated
const numbers = [1, 2, 3];      // array allocated

// Use — read and write
console.log(user.name);
numbers.push(4);

// Release — happens automatically when variables go out of scope
// or when nothing references the data anymore

The Stack and the Heap

JavaScript uses two memory regions — the stack and the heap.

Stack — fast, fixed size, stores:

  • Primitive values — numbers, strings, booleans, null, undefined
  • Function call frames — local variables and parameters
  • References to objects (not the objects themselves)

Heap — slower, dynamic size, stores:

  • Objects
  • Arrays
  • Functions
  • Any reference type
// Stack — stored directly
let age = 22;
let isLoggedIn = true;
const PI = 3.14;

// Heap — object lives in heap, reference lives in stack
const user = { name: "Ali", age: 22 };
//    ↑ reference on stack    ↑ object in heap

When a function is called, a stack frame is created with all its local variables. When the function returns, that frame is removed from the stack — those variables are gone. Objects in the heap stay until the garbage collector removes them.


Garbage Collection

JavaScript uses a garbage collector — a background process that periodically finds and removes objects that are no longer needed.

The main algorithm used in modern JavaScript engines is mark-and-sweep.

Mark and Sweep

GC starts Mark all rootsglobal vars, call stack Follow all referencesfrom roots Mark every reachable object Sweep — delete unmarked objects Free memory released
  1. Mark — start from roots (global variables, current call stack) and mark every object reachable from them
  2. Sweep — remove everything that was not marked — unreachable objects

An object is reachable if it can be accessed from a root through any chain of references. If nothing can reach it — it is garbage and gets collected.

let user = { name: "Ali" };
// user object is reachable — user variable references it

user = null;
// nothing references the object anymore
// it is unreachable — garbage collector removes it

Reachability — The Core Concept

The garbage collector only removes unreachable objects. As long as something holds a reference to an object, it stays in memory.

// Single reference
let user = { name: "Ali" };
user = null; // unreachable — collected

// Multiple references
let user = { name: "Ali" };
let admin = user; // two references to the same object

user = null; // still reachable through admin
admin = null; // now unreachable — collected

Interconnected objects

function createFamily() {
  const parent = { name: "Parent" };
  const child = { name: "Child" };

  parent.child = child;   // parent references child
  child.parent = parent;  // child references parent

  return parent;
}

let family = createFamily();
// family → parent ↔ child — all reachable

family = null;
// nothing references parent anymore
// even though parent and child reference each other —
// neither is reachable from any root
// both get collected

The garbage collector is smart enough to detect circular references that have no path from a root. Both objects get collected even though they reference each other.


Memory Leaks

A memory leak happens when memory that is no longer needed is never released — because something still holds a reference to it, preventing garbage collection.

JavaScript leaks are subtle. The code works — it just uses more and more memory over time until the browser tab crashes or becomes unresponsive.

Leak 1 — Accidental global variables

function createData() {
  // ❌ Missing let/const/var — becomes global
  data = new Array(100000).fill("leak");
}

createData();
// data is now a global variable
// it lives forever — never garbage collected
function createData() {
  // ✅ Properly scoped — collected when function returns
  const data = new Array(100000).fill("ok");
}

Always declare variables with const or let. Global variables live for the entire lifetime of the page.

Leak 2 — Forgotten event listeners

function setupButton() {
  const btn = document.getElementById("btn");
  const largeData = new Array(100000).fill("data");

  // ❌ Event listener holds reference to largeData through closure
  btn.addEventListener("click", () => {
    console.log(largeData[0]);
  });
}

// Called many times — each call adds a new listener and a new largeData
setupButton();
setupButton();
setupButton();
// Three listeners, three largeData arrays — all in memory forever
function setupButton() {
  const btn = document.getElementById("btn");

  // ✅ Named function — can be removed
  function handleClick() {
    console.log("clicked");
  }

  btn.addEventListener("click", handleClick);

  // Clean up when done
  return () => btn.removeEventListener("click", handleClick);
}

const cleanup = setupButton();
// Later when the component is destroyed
cleanup();

Leak 3 — Detached DOM elements

// ❌ Keep reference to removed DOM element
let detachedNode;

function createNode() {
  const node = document.createElement("div");
  node.textContent = "I will leak";
  document.body.appendChild(node);
  detachedNode = node; // save reference
  document.body.removeChild(node); // remove from DOM
}

createNode();
// node is removed from DOM but detachedNode still references it
// the node and its subtree cannot be garbage collected
// ✅ Release the reference when done
function createNode() {
  const node = document.createElement("div");
  document.body.appendChild(node);
  document.body.removeChild(node);
  // no external reference kept — node can be collected
}

Leak 4 — Closures holding large data

// ❌ Closure keeps large data alive unnecessarily
function processData() {
  const largeArray = new Array(1000000).fill("data");

  return function() {
    // Only needs largeArray[0] but keeps all 1M items alive
    return largeArray[0];
  };
}

const getFirst = processData(); // largeArray stays in memory
// ✅ Extract only what you need
function processData() {
  const largeArray = new Array(1000000).fill("data");
  const firstItem = largeArray[0]; // extract what we need

  return function() {
    return firstItem; // only firstItem is kept alive
  };
}

const getFirst = processData(); // largeArray is collected

Leak 5 — Forgotten timers

// ❌ setInterval that never stops
function startPolling() {
  const data = fetchLargeData();

  setInterval(() => {
    process(data); // data stays alive because interval holds reference
  }, 1000);
}

startPolling();
// interval runs forever — data never collected
// ✅ Store the interval ID and clear it when done
function startPolling() {
  const data = fetchLargeData();

  const intervalId = setInterval(() => {
    process(data);
  }, 1000);

  // Return cleanup function
  return () => clearInterval(intervalId);
}

const stopPolling = startPolling();

// When done — clear the interval
stopPolling(); // data can now be collected

Leak 6 — Growing Maps and Sets

// ❌ Cache that grows forever
const cache = new Map();

function cacheResult(key, value) {
  cache.set(key, value); // never cleaned up
}

// Called thousands of times — cache grows indefinitely
// ✅ Bounded cache — limit size
const MAX_CACHE_SIZE = 100;

const cache = new Map();

function cacheResult(key, value) {
  if (cache.size >= MAX_CACHE_SIZE) {
    // Remove oldest entry
    const firstKey = cache.keys().next().value;
    cache.delete(firstKey);
  }
  cache.set(key, value);
}

Or use WeakMap when keys are objects — entries are collected automatically when objects are no longer referenced:

// ✅ WeakMap — automatic cleanup
const cache = new WeakMap();

function cacheForElement(element, data) {
  cache.set(element, data);
  // When element is removed from DOM and dereferenced
  // cache entry is automatically collected
}

Detecting Memory Leaks

Chrome DevTools Memory tab

  1. Open DevTools — F12
  2. Go to the Memory tab
  3. Take a Heap Snapshot — shows all objects in memory
  4. Perform actions in your app
  5. Take another snapshot
  6. Compare — objects that grew significantly may be leaks

Performance monitor

  1. Open DevTools
  2. Go to Performance tab
  3. Click record
  4. Use your app
  5. Stop recording
  6. Look at the JS Heap line — if it keeps growing without coming back down — you have a leak

performance.memory (Chrome only)

console.log(performance.memory);
// {
//   jsHeapSizeLimit: 2172649472,
//   totalJSHeapSize: 23068672,
//   usedJSHeapSize: 18234567   ← watch this number
// }

Best Practices for Memory Efficiency

// 1. Always declare variables with const or let
const data = [];  // not data = []

// 2. Clean up event listeners when components are destroyed
const cleanup = () => element.removeEventListener("click", handler);

// 3. Clear timers when done
const id = setTimeout(fn, 1000);
clearTimeout(id);

// 4. Null out references when done with large objects
let largeData = processFile(file);
doSomething(largeData);
largeData = null; // allow collection

// 5. Use WeakMap/WeakSet for object metadata
const metadata = new WeakMap(); // auto-cleanup

// 6. Avoid storing large data in closures
function process(data) {
  const needed = data.summary; // extract only what's needed
  return () => needed;          // closure holds only the summary
}

// 7. Limit cache sizes
const cache = new Map();
const MAX = 50;
function addToCache(key, val) {
  if (cache.size >= MAX) cache.delete(cache.keys().next().value);
  cache.set(key, val);
}

A Real Example — Component Cleanup

In single-page applications, components mount and unmount. If you do not clean up, each mount adds more event listeners, timers, and subscriptions — all leaking memory.

class DataDashboard {
  constructor(container) {
    this.container = container;
    this.data = [];
    this.pollingInterval = null;
    this.resizeHandler = this.handleResize.bind(this);
    this.abortController = new AbortController();

    this.init();
  }

  init() {
    // Start polling
    this.pollingInterval = setInterval(() => {
      this.fetchData();
    }, 5000);

    // Listen for resize
    window.addEventListener("resize", this.resizeHandler);

    // Fetch with abort signal — can be cancelled
    this.fetchData();
  }

  async fetchData() {
    try {
      const response = await fetch("/api/data", {
        signal: this.abortController.signal // ← can abort this
      });
      this.data = await response.json();
      this.render();
    } catch (error) {
      if (error.name === "AbortError") return; // expected — we aborted
      console.error("Fetch failed:", error);
    }
  }

  handleResize() {
    this.render();
  }

  render() {
    this.container.innerHTML = this.data
      .map(item => `<div>${item.name}: ${item.value}</div>`)
      .join("");
  }

  // Call this when the component is removed
  destroy() {
    // Clear the polling interval
    clearInterval(this.pollingInterval);

    // Remove event listener
    window.removeEventListener("resize", this.resizeHandler);

    // Cancel any in-flight fetch
    this.abortController.abort();

    // Release data reference
    this.data = null;

    console.log("Dashboard cleaned up — no memory leaks");
  }
}

const dashboard = new DataDashboard(document.getElementById("dashboard"));

// When navigating away or unmounting
dashboard.destroy();

Every resource acquired in init() is released in destroy(). Interval cleared, listener removed, fetch cancelled, data released. Nothing leaks.


Summary

  • JavaScript manages memory automatically through garbage collection
  • The mark-and-sweep algorithm removes objects that are no longer reachable from any root
  • An object is reachable if it can be accessed through any chain of references from a root
  • Primitives live on the stack — objects live on the heap
  • Common memory leaks — accidental globals, forgotten event listeners, detached DOM nodes, closures holding large data, forgotten timers, unbounded caches
  • Use WeakMap and WeakSet for object metadata — entries are collected automatically
  • Use Chrome DevTools Memory tab to detect leaks — take heap snapshots and compare
  • Always clean up — remove event listeners, clear timers, cancel requests when components unmount
  • Extract only what you need from large objects before closing over them in functions

On this page