Memory Management

Reviewed & published by Brayan K

Memory management in JavaScript is the automatic process by which the engine allocates memory for your variables and objects, then frees it through garbage collection once that data is no longer reachable.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Master how JavaScript engines handle memory allocation, garbage collection, and performance optimization

What You'll Learn in This Lesson

🗑️ Real-World Analogy: Hotel Housekeeping

Think of JavaScript memory like a hotel. When guests (variables) check in, they're assigned rooms (memory). The housekeeping staff (garbage collector) can only clean rooms that are completely vacated — if any guest still has a key (reference) to a room, housekeeping can't touch it. Memory leaks happen when guests check out but forget to return their keys — the room stays reserved forever, even though no one's using it.

📋 Common Memory Leak Sources:

Leak TypeCauseFix
Forgotten TimerssetInterval never clearedCall clearInterval on cleanup
Detached DOM NodesReference to removed elementsSet references to null
ClosuresInner function holds outer scopeNull out large objects when done
Event ListenersNever removed on component unmountremoveEventListener on cleanup

Understanding JavaScript Memory

JavaScript may look simple on the surface, but under the hood it handles memory in a way that directly affects app performance, frame rate, responsiveness, and long-term stability. Every variable you create, every object you allocate, every function you pass around — they all consume memory in the JavaScript engine.

Understanding how memory is allocated, tracked, and eventually released by the garbage collector is one of the keys to writing fast, efficient, and scalable JavaScript applications.

Memory Allocation Lifecycle

Every value in JavaScript goes through a three-phase lifecycle:

let user = { name: "Alex" }; // memory allocated
console.log(user.name);      // memory used
user = null;                 // memory can now be collected

The 3 Major Memory Regions

JavaScript engines divide memory into multiple segments, each optimized for different purposes:

A) The Stack (Primitives & Function Execution)

let x = 10;   // stored directly on stack
let isActive = true; // stack
let count = 42; // stack

B) The Heap (Objects, Arrays, Functions)

const user = { id: 1, name: "Sam" }; // heap
const arr = [1, 2, 3]; // heap
const fn = () => {}; // heap

The variable user on the stack contains a reference pointing to this object in the heap.

C) The Call Stack (Execution Context Tracker)

This tracks the order of function calls. Every time you call a function, a new stack frame is pushed.

function a() { b(); }
function b() { c(); }
function c() { return 10; }

a(); // stack: a -> b -> c

As functions return, their stack frames are popped, freeing local variables.

Reachability — The Core Rule of Garbage Collection

The garbage collector frees memory only when a value becomes unreachable, meaning there are no references pointing to it.

Main Sources of Reachability:

function keepReference(target) {
  // "target" is a parameter, so it is its own binding holding its own
  // reference to the object. Nulling "obj" outside cannot reach in and clear it.
  return function () {
    console.log(target.a);
  };
}

let obj = { a: 100 };
const fn = keepReference(obj);   // the closure now has a reference of its own

obj = null;                      // your handle on the object is gone...
console.log(String(obj));        // null

fn();                            // ...but the closure still reaches it -> 100

// ✅ Expected output:
// null
// 100

Worked Example: Follow Every Reference

Read this one line by line before you run it. It walks through the three situations that decide whether memory is freed: two variables pointing at one object, a closure that captured a reference, and a closure that captured a variable. Every comment says what that line prints and why.

// Reachability in action — what "no longer referenced" really means.
// The collector frees an object only when NOTHING can still reach it.
// You cannot watch the collector run, but you can watch which handles survive.

const profile = { name: "Alex", visits: 1 };   // one object on the heap

// "alias" does NOT copy the object. It copies the reference — the address.
// Both variables now point at the SAME object.
let alias = profile;

alias.visits = 2;                              // change it through one handle...
console.log("via profile:", profile.visits);   // ...the other sees it: 2

// Dropping one handle frees nothing while another handle survives.
alias = null;
console.log("alias is now", String(alias));    // null
console.log("profile still holds it:", profile.name);   // Alex

// A closure is a handle too. "hold" captures the parameter "target",
// which is a binding of its own, separate from the variable outside.
function hold(target) {
  return function () {
    return target.label;
  };
}

let data = { label: "big-report" };
const reader = hold(data);   // the closure takes its own reference

data = null;                 // your handle is gone...
console.log("data is now", String(data));                     // null
console.log("but the closure kept it alive:", reader());      // big-report

// That is a leak in miniature: you can no longer get at the object,
// the engine still can, so it stays in memory until "reader" itself dies.

// Contrast — capturing the OUTER VARIABLE instead of a copy of the reference.
let doc = { label: "draft" };
const readOuter = () => doc;   // reads the variable "doc" at call time
doc = null;                    // this really does break the link
console.log("readOuter() now returns", String(readOuter()));  // null

// ✅ Expected output:
// via profile: 2
// alias is now null
// profile still holds it: Alex
// data is now null
// but the closure kept it alive: big-report
// readOuter() now returns null

Memory Leak Types That Every Developer Must Avoid

These are the most common memory leaks that destroy production applications:

Leak Type #1 — Accidental Global Variables

function demo() {
  badVar = 123; // no 'let', 'const', or 'var' => becomes global!
}

demo(); // badVar is now global and never freed

These stay alive until page reload and clog memory.

Leak Type #2 — Forgotten Timers & Intervals

// BAD: Never cleared
setInterval(() => console.log("running"), 1000);

// GOOD: Cleared when done
const id = setInterval(() => console.log("running"), 1000);
clearInterval(id);

Leak Type #3 — Detached DOM Nodes

let cached = document.getElementById("btn");
cached.remove(); // removed from DOM

// cached still holds the object → memory not freed

Leak Type #4 — Growing Data Structures

const cache = [];

function add() {
  cache.push(new Array(10000).fill("*"));
}

// Unbounded growth = guaranteed memory leak

The fix for an unbounded cache is a size limit plus an eviction rule: when the cache grows past its limit, throw the oldest entry away. Finish the version below — the structure is written for you, and only the three decisions that make it bounded are missing.

// 🎯 YOUR TURN — stop a cache from growing forever
// A cache with no limit is Leak Type #4. Give this one a hard ceiling.
// Replace each ___ then press Run.

function createBoundedCache(maxSize) {
  // A Map remembers insertion order, so the FIRST key it gives back is the oldest.
  const store = new Map();

  return {
    set(key, value) {
      store.set(key, value);

      // 1) Has the cache outgrown its limit?
      if (store.size > ___) {                // 👉 compare against the limit

        // 2) Grab the oldest key (first one out of the iterator).
        const oldest = store.keys().next().value;

        // 3) Drop it, so the cache stays a fixed size forever.
        store.___(oldest);                   // 👉 which Map method removes an entry?
        console.log("evicted:", oldest);
      }
    },
    get(key) {
      return store.get(key);
    },
    size() {
      return store.___;                      // 👉 Map exposes its count as a property
    }
  };
}

const cache = createBoundedCache(3);
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
cache.set("d", 4);   // this one tips it over the limit
cache.set("e", 5);

console.log("size:", cache.size());
console.log("a is", String(cache.get("a")));
console.log("e is", String(cache.get("e")));

// ✅ Expected output:
// evicted: a
// evicted: b
// size: 3
// a is undefined
// e is 5

Mark-and-Sweep: The Core GC Algorithm

Nearly all JavaScript engines use variations of the Mark-and-Sweep algorithm, which happens in three phases:

Phase 1: Root Discovery

The engine identifies "roots" — memory that must never be collected:

Phase 2: Mark Phase

V8 traverses all reachable objects starting from the roots. Every reachable object gets a "marked" flag:

let a = { value: 1 };
let b = { parent: a };
let c = { parent: b }; // reachable through b → a

// All marked as reachable
// Unreachable objects left unmarked = "dead"

Phase 3: Sweep Phase

V8 removes all unmarked objects:

Generational GC — Young vs Old Objects

V8 uses a Generational Garbage Collector, dividing memory into "generations" based on object lifetime:

Young Generation (New Space)

function compute() {
  let temp = { x: 1, y: 2 };
  return temp.x + temp.y;
} // temp lives in young space

Old Generation (Old Space)

const cache = {}; 
// long-lived, old-generation

const config = { apiUrl: "..." };
// promoted to old space

Writing GC-Friendly Code: Best Practices

Modern JavaScript engines are smart, but the code you write has a major impact on GC performance:

✅ Prefer Local Variables (Short-Lived Objects)

function calculate() {
  const temp = { score: 100 };
  return temp.score;
}

// Short-lived objects = efficiently collected by Scavenge GC
// Long-lived objects = expensive for Mark-Compact

✅ Nullify Unused References Immediately

let bigObject = { /* huge data */ };

// Use the object...
processData(bigObject);

// Done? Release it immediately
bigObject = null; // tells V8 it can be swept

✅ Reuse Arrays Instead of Reallocating

// BAD: Creates new array
arr = [];

// GOOD: Reuses existing array
arr.length = 0; // fast and GC-friendly

✅ Use WeakMap/WeakSet for Automatic Cleanup

let wm = new WeakMap();

(function () {
  let obj = {};
  wm.set(obj, "value");
})(); // obj lost → automatically freed

// Weak collections = best tool for automatic cleanup

✅ Object Pooling for Games/Heavy Apps

class BulletPool {
  constructor(size) {
    this.pool = Array.from({ length: size }, () => new Bullet());
  }
  get() {
    return this.pool.pop() || new Bullet();
  }
  release(bullet) {
    this.pool.push(bullet);
  }
}

// Benefits: avoids GC, boosts FPS, smoother gameplay

Debugging Memory Leaks Professionally

Chrome DevTools provides powerful memory debugging capabilities:

Tool 1: Heap Snapshot

Look for: Detached DOM trees, retained closures, large arrays

Tool 2: Allocation Timeline

This measures memory over time. If the graph keeps rising → leak detected.

Tool 3: Performance Monitor

If memory keeps rising during idle → leak confirmed.

🎯 Mini-Challenge: A Teardown Helper

Nearly every leak in this lesson is the same mistake wearing a different hat: you registered something and never unregistered it. A timer, a listener, a socket. The professional answer is not "remember harder" — it is a small helper that collects cleanup functions as you go and runs the whole lot in one call.

Nothing is filled in this time. You get the brief and a fixed test drive; if your output matches the expected output at the bottom, your helper works.

// 🎯 MINI-CHALLENGE: a teardown helper
//
// Write createScope() from scratch. Only the outline is here.
//
// createScope() returns an object with exactly two methods:
//   add(name, cleanupFn)  -> remember a cleanup function under that name
//   destroy()             -> run EVERY stored cleanup function, in the order
//                            they were added, then forget all of them, so a
//                            second destroy() does nothing at all
//
// Keep the stored functions private — nothing outside should be able to
// reach the list except through those two methods.
//
// Hint: an array inside the closure, and reassigning it to [] in destroy().

// your code here


// --- Do not change the code below: this is the test drive ---
const scope = createScope();
scope.add("timer",    () => console.log("cleared the interval"));
scope.add("listener", () => console.log("removed the click listener"));
scope.add("socket",   () => console.log("closed the socket"));

console.log("--- tearing down ---");
scope.destroy();
console.log("--- destroying again ---");
scope.destroy();
console.log("done");

// ✅ Expected output:
// --- tearing down ---
// cleared the interval
// removed the click listener
// closed the socket
// --- destroying again ---
// done

Key Takeaways

JavaScript engines use tracing garbage collection with mark-and-sweep algorithms

V8 optimizes memory using generational GC: young objects are cheap, old objects are expensive

Leaks commonly come from closures, listeners, timers, caches, and detached DOM nodes

Chrome DevTools is your primary weapon for diagnosing memory issues

Mobile memory limits require extra careful approaches to allocation and cleanup

Writing GC-friendly code dramatically improves performance and user experience

Real-world leaks are subtle, invisible, and can destroy production applications

Understanding memory management makes you a senior-level JavaScript developer

Practice quiz

What are the three phases of the memory lifecycle?

  • Open, edit, close
  • Read, write, delete
  • Allocation, usage/retention, release/reclamation
  • Push, pop, shift

Answer: Allocation, usage/retention, release/reclamation. Memory is allocated, used/retained, then released by the garbage collector when unreachable.

Where are primitives like numbers and booleans stored?

  • The stack
  • The heap
  • The cloud
  • The DOM

Answer: The stack. Primitives and pointers live on the fast, small stack; objects live on the heap.

When does the garbage collector free a value?

  • Every second
  • When the variable is declared
  • Never, automatically
  • Only when the value becomes unreachable (no references point to it)

Answer: Only when the value becomes unreachable (no references point to it). GC reclaims memory only when a value is unreachable.

Why can a closure cause memory NOT to be freed even after obj = null?

  • null is invalid
  • The closure still holds a reference to the data
  • Closures disable GC globally
  • The stack overflows

Answer: The closure still holds a reference to the data. If a closure captures the object, a reference remains and the memory stays alive.

Which is a common memory leak source listed in the lesson?

  • Forgotten timers (setInterval never cleared)
  • Using const
  • Returning early
  • Using map()

Answer: Forgotten timers (setInterval never cleared). A setInterval that is never cleared keeps running and retaining memory.

What core algorithm do nearly all JavaScript engines use for garbage collection?

  • Quicksort
  • Reference doubling
  • Mark-and-Sweep
  • Binary search

Answer: Mark-and-Sweep. Mark-and-Sweep marks reachable objects from roots, then sweeps the unmarked ones.

In V8's generational GC, where do short-lived objects live?

  • Old generation
  • Young generation (new space)
  • The call stack only
  • Disk

Answer: Young generation (new space). Short-lived objects live in the young generation and are collected cheaply and frequently.

Why is a missing variable declaration (badVar = 123 inside a function) a leak?

  • It is a syntax error
  • It uses too much CPU
  • It blocks the event loop
  • It becomes an accidental global that is never freed

Answer: It becomes an accidental global that is never freed. Without let/const/var it becomes a global variable that survives until page reload.

Which tool is recommended for automatic cleanup of references in the lesson?

  • Array
  • WeakMap/WeakSet
  • JSON
  • Set with manual delete

Answer: WeakMap/WeakSet. WeakMap/WeakSet hold weak references that are released automatically when keys are gone.

What is the recommended fix for a detached DOM node that JavaScript still references?

  • Reload the page
  • Add more listeners
  • Set the reference to null
  • Use innerHTML

Answer: Set the reference to null. Setting the reference to null lets the GC reclaim the removed DOM node.

Continue this course