cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Why garbage collection?

Learn why JavaScript garbage collection uses roots, reachability, object graphs, pauses, and trade-offs to reclaim memory safely.

By the end, you can
  • 01
    Explain reachabilityTrace from roots through an object graph and decide which objects are reachable or garbage, including detached cycles.
  • 02
    Compare memory modelsContrast manual allocation with automatic garbage collection and identify what JavaScript developers still control.
  • 03
    Reason about GC trade-offsConnect throughput, latency, memory headroom, and stop-the-world pauses to real browser responsiveness.

Why garbage collection exists

Every object, array, function, closure, and DOM wrapper needs memory while your program can still use it. The hard part is knowing when that memory is no longer needed. JavaScript chooses automatic memory management: the engine and host cooperate to find objects the program can no longer reach, then reclaim their space.

Definition

Garbage collection is automatic memory reclamation. A tracing collector starts from roots, follows references through the object graph, keeps reachable objects, and treats unreachable objects as garbage.

A root is a starting point the runtime must treat as live, such as globals, current stack-frame locals, and host-held handles for timers or event listeners. Reachability means there is a path from a root to an object by following references. The object graph is the network formed by those objects and references.

This module begins after Exceptions & unwinding. The everyday memory lessons already cover memory management, stack and heap, WeakMap and WeakSet, WeakRef and finalization, and finding memory leaks. Here we go inside the engine. Heap layout is the companion lesson for heap spaces; later lessons will cover reference counting, mark-sweep, generational GC, write barriers, and incremental or concurrent GC.

Manual vs automatic memory management

Manual memory management gives the program direct responsibility. In C, code can ask for memory with malloc and return it with free. That control is useful, but the code must be exactly right. Using memory after it was freed, freeing it twice, or forgetting to free it can crash or leak.

Manual memory hazards in CC
// C example: manual memory management, not browser JavaScript.char *name = malloc(32);strcpy(name, "Asha");free(name);printf("%s", name);     // bug: use-after-freefree(name);             // bug: double free// If free(name) were forgotten, this allocation would leak.

JavaScript does not expose free(object). Instead, a collector decides when an object is unreachable. Line 2 below creates an object inside makeReceiptReader. Line 3 returns a function that still reads it. Because readTotal remains reachable on line 8, the receipt must remain reachable too.

Automatic memory keeps reachable closure dataPop out in the code editor (opens in a new tab)JavaScript
function makeReceiptReader() {  const receipt = { customer: "Asha", total: 1200 };  return function readTotal() {    return receipt.total;  };} const readTotal = makeReceiptReader();console.log(readTotal());
Manual and automatic memory at a glance
ModelWho decides to release?Main risk
Manual memory managementThe program asks for memory and must release it later.Fast and explicit, but mistakes include leaks, use-after-free, and double free.
Automatic garbage collectionThe runtime frees objects that can no longer be reached from roots.Safer for everyday code, but the collector spends time, uses metadata, and needs headroom.
JavaScript developer's jobCreate clear ownership, remove unused listeners/timers, and avoid retaining large graphs accidentally.You do not call free, but you still choose which references stay alive.

Automatic memory management removes an entire class of manual-memory bugs, but it does not make memory free. The collector needs time, metadata, and spare heap headroom. Your code still decides which references stay alive.

Roots and reachability

Reachability is a graph question. If a root can reach an object by following references, the object must be kept. If no root can reach it, it is garbage, even when it points to other unreachable objects.

A tiny reachability tracePop out in the code editor (opens in a new tab)JavaScript
const graph = {  root: ["cart"],  cart: ["items"],  items: [],  detachedA: ["detachedB"],  detachedB: ["detachedA"],}; function markFrom(roots) {  const marked = new Set();  const worklist = [...roots];  while (worklist.length > 0) {    const node = worklist.pop();    if (marked.has(node)) continue;    marked.add(node);    for (const child of graph[node]) worklist.push(child);  }  return [...marked].sort();} console.log(markFrom(["root"]).join(","));

The snippet prints cart,items,root. The detached pair detachedA and detachedB is not printed, because no root path leads to either object. The cycle between them does not matter to a tracing collector.

Step through marking from roots
Step 0 of 8Ready
Your turn: follow the blue line

Step through a real mark-from-roots teaching model. It does not inspect the browser engine; it shows the reachability decision tracing collectors make.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  globalThis: ["cart"],  cart: ["items"],  items: [],  detachedA: ["detachedB"],  detachedB: ["detachedA"],  timer: ["callback"],  callback: ["cache"],  cache: [],};const roots = ["globalThis", "timer"]; function markFromRoots(names) {  const marked = new Set();  const worklist = [...names];  while (worklist.length > 0) {    const name = worklist.pop();    if (marked.has(name)) continue;    marked.add(name);    for (const child of graph[name]) worklist.push(child);  }  return [...marked].sort();} console.log(markFromRoots(roots).join(","));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Real-life analogyBalloons on strings

A balloon stays while a string connects it to someone's hand. Two balloons tied only to each other float away because no hand reaches them.

In real life: A hand holds a balloon string
In JavaScript: A root starts a reachability trace
In real life: Strings connect balloons
In JavaScript: References form a path through objects
In real life: Two loose balloons are tied together
In JavaScript: An unreachable cycle is still garbage
In real life: A balloon still reaches a hand
In JavaScript: Reachable objects survive collection

Where the analogy stops: A collector follows references mechanically. It cannot decide whether a reachable object is useful to your program.

The object graph is the collector's map

Real applications are bigger than the tiny snippet: a root might point to a cart, the cart might point to items, a detached pair might form a cycle, and an event listener might keep a closure alive. The collector sees one graph, not separate stories.

Playground: toggle references in an object graph
References controlled by the playgroundJavaScript
const roots = ["window", "event-listener"]; window.cart = cart;              // toggle: root -> cartcart.items = items;              // toggle: cart -> itemsdetachedA.friend = detachedB;    // cycle half 1detachedB.friend = detachedA;    // cycle half 2button.onclick = () => draftOrder.total; // toggle: listener -> closure data const result = markFromRoots(roots, graph);console.log(result.reachable, result.garbage);
Trace result2 garbage
rootswindow, event-listener

Roots start the trace.

reachablecart, draft-order, event-listener, items, listener-closure, window

These objects would survive this collection.

garbagedetached-a, detached-b

These objects have no root path.

visit orderevent-listener → listener-closure → draft-order → window → cart → items

A real engine may visit in a different order; the final set is what matters.

windowroot

root: global object

References: cart

cartreachable

shopping cart object

References: items

itemsreachable

array of item objects

References: none

detached-agarbage

detached cycle A

References: detached-b

detached-bgarbage

detached cycle B

References: detached-a

event-listenerroot

root: host event listener

References: listener-closure

listener-closurereachable

closure data captured by a handler

References: draft-order

draft-orderreachable

large draft order

References: none

Try it yourself

The model marks 6 objects and leaves 2 as garbage. The detached cycle is garbage because no root reaches it.

This is a teaching model; it does not inspect your browser's engine. It runs the lesson's mark-from-roots function over a small graph.
Real-life analogyA WhatsApp group

Start with one person in a WhatsApp group and follow each saved contact. Anyone connected by that chain can be reached; two people connected only to each other cannot.

In real life: A group member starts the contact chain
In JavaScript: Roots start the trace
In real life: Contacts introduce more contacts
In JavaScript: Reachable objects lead to more reachable objects
In real life: Two people know only each other
In JavaScript: Detached cycles are garbage
In real life: One saved contact reaches many others
In JavaScript: A closure or listener can retain a whole subgraph

Where the analogy stops: People choose who to contact. A collector only follows stored references.

This is why leak debugging often means looking for the first accidental root, not the biggest object. A large array is only a symptom if a root path still reaches it.

Real V8 evidence: weak-only objects can disappear

Browser JavaScript cannot force a collection, and production code should not depend on exactly when collection runs. For a lesson test, Node can expose V8's test hook with --expose-gc. The probe below waits for later tasks between gc() calls, because weak targets are cleared after the current job, not while your current expression is still using them.

Node-only WeakRef reachability proofJavaScript
const wait = () => new Promise((resolve) => setTimeout(resolve, 0)); function weakOnly() {  let target = { label: "weak-only", data: new Array(1000).fill("x") };  const weak = new WeakRef(target);  target = null;  return weak;} const rooted = { label: "rooted", data: new Array(1000).fill("y") };const rootedRef = new WeakRef(rooted);const weakRef = weakOnly();let collected = false; for (let attempt = 0; attempt < 8; attempt += 1) {  await wait();  globalThis.gc();  await wait();  if (weakRef.deref() === undefined) {    collected = true;    break;  }} console.log(  `Node ${process.versions.node} (V8 ${process.versions.v8.split(".").slice(0, 2).join(".")})`,);console.log("weak collected", collected);console.log("root survives", rootedRef.deref()?.label);
Verified runtime fact

The lesson test runs this code in a child Node 22 process with V8 12.4. It asserts that a weak-only object is collected after later-task gc() retries, while an object still strongly reachable from a root survives as rooted.

Use this as evidence of the reachability rule, not as an app technique. For everyday weak-reference patterns, link back to the published WeakRef and finalization lesson.

What the collector must balance

A collector has two jobs that pull against each other. It must find garbage accurately, and it must do that work without making the program feel slow or using too much memory. Engine teams choose algorithms and scheduling strategies to balance three pressures: throughput, latency, and memory overhead.

Throughput asks how much total time is left for useful work. Latency asks how long any one pause lasts. Memory overhead asks how much extra heap headroom, metadata, or copied space the strategy needs. The next lessons explore the individual algorithms; this first lesson keeps the trade-off vocabulary sharp.

A teaching model, not an engine dump

The executions and playgrounds on this page are honest models. They run real JavaScript functions from the lesson, but they do not inspect your browser's heap, card tables, nursery, or old space.

Throughput, latency, and memory trade-offs

The same allocation pattern can be collected in different ways. Rare big collections keep JavaScript running longer between collections, but when the collector finally runs the pause can be large. Frequent small collections lower the worst pause, but may spend more total time collecting. More memory headroom can reduce collection frequency, but increases peak memory.

Three GC pressures to keep separate
PressureQuestionTypical compromise
ThroughputHow much total time the program spends doing useful work instead of collecting.A collector may batch work to reduce total overhead.
LatencyHow long one individual pause or slice lasts.Shorter pauses matter for typing, scrolling, animation, and input response.
Memory overheadExtra memory kept as headroom, metadata, copied spaces, or not-yet-reclaimed garbage.More headroom can improve throughput but raises peak memory.
Pause placementWhether work happens all at once, incrementally, concurrently, or during idle periods.JavaScript must observe the same language behavior either way.
Playground: choose a collection strategy
Strategy simulatorJavaScript
const bursts = [10, 12, 9, 14, 8, 11, 13, 7, 12, 10]; function simulate(strategy) {  let heap = 20;  let totalPause = 0;  for (const allocated of bursts) {    heap += allocated;    if (heap >= strategy.threshold) {      totalPause += strategy.pause(heap);      heap = strategy.reclaimTo;    }  }  return totalPause;} console.log(simulate(frequentSmall));
Modeled resultBalanced collections
Rare big collections22 ms worst

1 collections · 22 ms total GC · 84 MB peak heap

Balanced collections13.9 ms worst

2 collections · 27.1 ms total GC · 65 MB peak heap

Frequent small collections7.8 ms worst

5 collections · 37.7 ms total GC · 43 MB peak heap

Burst 413.9 ms pause

Heap 65 MB → 22 MB after collecting.

Burst 813.2 ms pause

Heap 61 MB → 22 MB after collecting.

Step 2 of 6Balanced collections

Balanced collections: 2 collections, 27.1 ms total GC time, 13.9 ms worst pause, and 65 MB peak heap in this model.

This is a modeled trade-off, not a measurement of your browser. Real engines combine generational, incremental, concurrent, and idle-time techniques.

Modern engines mix techniques: later lessons will discuss generational collection, write barriers, and incremental or concurrent marking. For now, keep the vocabulary: throughput is total cost, latency is one pause, and memory overhead is headroom.

Stop-the-world pauses

A stop-the-world pause stops JavaScript while the collector performs work that must see a stable view. The page does not run your click handler or animation callback during that pause. This matters for 60 fps rendering, where one frame has about 16.7 ms for JavaScript, style, layout, paint, and compositing.

Step through a stop-the-world pause in a frame
Step 0 of 14Ready
Your turn: follow the blue line

Replay a frame-budget model. It shows why pause length matters even when total collection time is modest.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
let heapMB = 12; function renderFrame(workMs, allocatedMB) {  heapMB += allocatedMB;  if (heapMB >= 24) {    const pauseMs = 12;    heapMB = 10;    return workMs + pauseMs > frameBudgetMs ? "missed frame" : "smooth";  }  return workMs <= frameBudgetMs ? "smooth" : "missed frame";} console.log(renderFrame(5, 4));console.log(renderFrame(5, 4));console.log(renderFrame(5, 4));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay's third frame does only 5 ms of normal JavaScript work, but a 12 ms pause pushes it to 17 ms. That misses the 16.7 ms frame budget. If this idea is new, pair it with rendering performance.

Node-only --trace-gc probeJavaScript
const keep = [];for (let index = 0; index < 20000; index += 1) {  keep.push({ index, data: new Array(20).fill(index) });}globalThis.gc();console.log("kept objects", keep.length);
Verified V8 trace shape

The lesson test runs Node with --trace-gc --expose-gc. In this V8 12.4 build, the trace includes stable collection names such as Scavenge and Mark-Compact or Mark-sweep, plus pause milliseconds. The test does not assert exact addresses, heap sizes, or timings.

Practical developer habits

Most days, you do not tune the collector. You shape reachability. If a component is gone, remove listeners that close over its data. Clear timers and intervals. Drop cache entries when they stop being useful. Keep large temporary objects inside functions when possible so their stack roots disappear quickly.

  • Use heap snapshots and allocation timelines before changing code.
  • Remove unused event listeners, observers, and timers that keep closures reachable.
  • Prefer bounded caches; document what owns each long-lived collection.
  • Measure interaction latency before assuming GC is the bottleneck.
  • Link to the everyday lessons for API details instead of duplicating them here.

Garbage collection is a safety net, not a substitute for ownership. The collector frees unreachable memory; it cannot decide whether your global cache should still be important.

Root, reachable, or garbage?
  • globalThis or window at the start of a trace.
  • The cart local variable in the currently running function.
  • An items array referenced by a reachable cart object.
  • Data captured by a callback that a live timer still holds.
  • Two detached objects that only point at each other.
  • A cache object after the only event listener retaining it was removed.
Try it yourself
0 of 6 correct

Sort each card by its role during a tracing collection. Read each explanation even after a correct answer.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “Garbage means useless.” It means unreachable. A reachable object can still be a logical leak.
  • “Cycles always leak.” A detached cycle is garbage for a tracing collector. A cycle leaks only when a root path remains.
  • “GC runs immediately when a variable goes out of scope.” The object becomes eligible; the exact collection time is an engine choice.
  • “WeakRef is a cleanup tool for normal app logic.” It is for advanced cases. Do not depend on finalization timing.
  • “GC pauses are the same as total GC cost.” One long pause hurts latency even if total collection time is modest.
Similar GC ideas that are easy to confuse
IdeaWhat it meansDo not confuse it with
UnreachableNo path exists from any root to the object.The same as useless to a human; collectors cannot read your intent.
ReachableAt least one path from a root leads to the object.A guarantee the object will be used again.
RootA starting point the engine or host must treat as live, such as stack locals, globals, or host handles.Every object with a name in source code.
LeakMemory stays reachable longer than intended.Only something that GC fails to understand.

Practice exercises

Exercise 1 · Warm-upPredict the marked names

Read the code and type the comma-separated output.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const graph = { root: ["cart"], cart: ["items"], items: [], a: ["b"], b: ["a"] };
const marked = new Set();
const worklist = ["root"];
while (worklist.length) {
  const node = worklist.pop();
  if (marked.has(node)) continue;
  marked.add(node);
  for (const child of graph[node]) worklist.push(child);
}
console.log([...marked].sort().join(","));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead a weak collection while the key is rooted

    What does this print while strong still points at the key?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const strong = { name: "rooted" };
    const weak = new WeakMap();
    weak.set(strong, "metadata");
    console.log(weak.get(strong));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWrite the guard in a mark function

      Name the data structure that prevents a traversal from visiting the same object again.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function mark(node) {
        if (/* already visited? */) return;
        // mark node, then visit children
      }

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the accidental root

        Which reference should be removed to let draftOrder become collectible?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function mount(button) {
          const draftOrder = { lines: new Array(10000).fill("item") };
          button.addEventListener("click", () => console.log(draftOrder.lines.length));
        }
        // The component is removed, but the listener remains on the button.

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose for animation latency

          Which strategy from the playground would you try first for an animation jank problem?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const workMs = 5;
          const pauseMs = 12;
          console.log(workMs + pauseMs > 16.7 ? "missed frame" : "smooth");

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a real site

            Your checkout page grows memory during a long session. What should you do before rewriting the cart code?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Answer by naming roots, tracing references, and keeping throughput, latency, and memory separate.

              Garbage collection basics quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does a tracing garbage collector start from?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this reachability snippet print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const root = { name: "root" };
                root.cart = { items: ["tea"] };
                console.log(root.cart.items.length);

                Choose an answer to see the explanation.

              3. Question 3 of 7Two detached objects point at each other, but no root points at either one. What are they?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does this frame-budget model print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const workMs = 5;
                const pauseMs = 12;
                console.log(workMs + pauseMs > 16.7 ? "missed frame" : "smooth");

                Choose an answer to see the explanation.

              5. Question 5 of 7Which statement best describes automatic memory management in JavaScript?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which choice improves latency in the trade-off model?

                Choose an answer to see the explanation.

              7. Question 7 of 7What should a working developer usually do with this knowledge?

                Choose an answer to see the explanation.

              Key takeaways

              • Garbage collection frees objects the program can no longer reach from roots.
              • Roots include globals, current stack-frame locals, and host-held handles such as timers or listeners.
              • The object graph decides reachability; unreachable cycles are still garbage.
              • Collectors balance throughput, latency, and memory overhead, so pause length and total GC time are different questions.
              • Stop-the-world pauses halt JavaScript and can matter when a frame has only about 16.7 ms.
              • Developers should remove accidental roots and measure memory behavior before optimizing.

              Remember the one-liner.
              Garbage is not “old” memory; garbage is memory no root can reach.

              Coming next: Reference counting & cycles, the simple counting idea and why cycles make it tricky. Later lessons move into mark-sweep, generational collection, write barriers, and incremental or concurrent GC.

              CompleteFrontend Clear concepts. Working examples.