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
- Memory Lifecycle (Allocate, Use, Release)
- Stack vs Heap memory
- Garbage Collection algorithms (Mark-and-Sweep)
- Reference counting & reachability
- Common Memory Leaks & Fixes
- Profiling memory with Chrome DevTools
🗑️ 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 Type | Cause | Fix |
|---|---|---|
| Forgotten Timers | setInterval never cleared | Call clearInterval on cleanup |
| Detached DOM Nodes | Reference to removed elements | Set references to null |
| Closures | Inner function holds outer scope | Null out large objects when done |
| Event Listeners | Never removed on component unmount | removeEventListener 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 collectedThe 3 Major Memory Regions
JavaScript engines divide memory into multiple segments, each optimized for different purposes:
A) The Stack (Primitives & Function Execution)
- Stores numbers, booleans, null, undefined, pointers to objects
- Fast, small, and limited in size
- Perfect for simple values and call stack tracking
let x = 10; // stored directly on stack
let isActive = true; // stack
let count = 42; // stackB) The Heap (Objects, Arrays, Functions)
- Large memory pool for dynamic data
- Stores complex and dynamic data structures
- Values here must be garbage-collected
const user = { id: 1, name: "Sam" }; // heap
const arr = [1, 2, 3]; // heap
const fn = () => {}; // heapThe 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 -> cAs 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:
- Global object (window in browsers)
- Local variables inside active functions
- Variables stored in closures
- DOM elements referenced by JavaScript
- Timers and intervals referencing data
- Event listeners attached to elements
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
// 100Worked 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 nullMemory 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 freedThese 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 freedLeak Type #4 — Growing Data Structures
const cache = [];
function add() {
cache.push(new Array(10000).fill("*"));
}
// Unbounded growth = guaranteed memory leakThe 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 5Mark-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:
- Global object (window)
- Current call stack variables
- Active closures
- Event listeners
- Scheduled timers
- Web APIs holding references
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:
- Frees heap memory
- Reclaims space
- Updates allocation pointers
Generational GC — Young vs Old Objects
V8 uses a Generational Garbage Collector, dividing memory into "generations" based on object lifetime:
Young Generation (New Space)
- Small memory region
- Holds short-lived objects
- Cleaned frequently
- Extremely fast to allocate & collect
- Uses "scavenging" or "copying" GC
- Objects survive 1–2 cycles max
function compute() {
let temp = { x: 1, y: 2 };
return temp.x + temp.y;
} // temp lives in young spaceOld Generation (Old Space)
- Objects survive multiple GC cycles
- Long-lived data
- Requires slower, full GC
- Compacting may occur here
- Large structures live here
const cache = {};
// long-lived, old-generation
const config = { apiUrl: "..." };
// promoted to old spaceWriting 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 gameplayDebugging Memory Leaks Professionally
Chrome DevTools provides powerful memory debugging capabilities:
Tool 1: Heap Snapshot
- Open Chrome DevTools → Memory tab
- Take Snapshot #1
- Perform actions that might leak
- Take Snapshot #2
- Compare retained sizes
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
- JavaScript heap size
- Event listener count
- FPS (frames per second)
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 ---
// doneKey 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
- Previous: Pure Functions, Immutability & Side Effects
- Next: Deep Copy vs Shallow Copy & Data Structures — Clone objects safely and choose the right data structure for the job
- Quick reference: JavaScript cheat sheet