completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Garbage collection across engines & the DOM

Learn how JavaScript engines collect memory differently while tracing JavaScript and C++ DOM objects as one reachable graph.

By the end, you can
  • 01
    Compare how engines clean memoryName the main cleaning choices in V8, SpiderMonkey, JavaScriptCore, and Hermes.
  • 02
    Follow a DOM button's two partsExplain why a button's JavaScript wrapper and its C++ data must be traced together.
  • 03
    Explain conservative scanningExplain how a stack number that looks like an address can keep an object and pin it in place.

One language rule, many collectors

A garbage collector (GC for short) is the part of a JavaScript engine that frees memory your program no longer needs. An engine is the program that runs your JavaScript, like V8 in Chrome.

JavaScript itself gives only one rule. An object may be freed once your running program can no longer reach it. The language doesn't say when the cleaning happens, how memory is laid out, or whether objects get moved. Each engine chooses those things, together with the browser or app around it, called the host.

Definition

A tracing collector works in three steps:

  • It starts from the roots. Roots are places your program can always use, like global variables and the variables of functions that are running now.
  • It follows every reference, from one object to the next.
  • It keeps every object it reached. Those objects and the references between them are called the reachable graph.

A collector strategy is one engine's own way to split up that work: by the age of objects, into short pauses, onto helper threads, and into separate areas of memory.

A card that says one rule: keep what a root can reach. Four arrows go from it to four engines: V8, SpiderMonkey, JavaScriptCore, and Hermes.one rulekeep what a rootcan reachV8SpiderMonkeyJavaScriptCoreHermes
All four engines follow the same rule: keep what a root can reach. Each engine does the cleaning work in its own way.

Why does this matter? Your code should never count on a cleanup at one exact moment. But knowing the main strategies helps you read a memory tool. It also helps you see why a button you removed from the page can still stay in memory, held by a listener or a wrapper.

Real-life analogyDifferent home cleaning schedules

Different families clean their homes on different schedules. One family does a quick tidy every day. Another family tidies one small part at a time, between other chores. Both families keep the things someone still uses.

In real life: Every family keeps the things someone still uses
In JavaScript: Every engine keeps reachable objects
In real life: One family tidies new things every day
In JavaScript: An engine may clean new objects often
In real life: Another family tidies one room at a time
In JavaScript: An engine may clean in small pieces
In real life: The house rule stays the same
In JavaScript: JavaScript's rule doesn't pick a cleaning method

When you read an article about an engine, sort each sentence into one of two groups:

  • A language rule tells you what your program can see. Example: an object can no longer be used once the last path to it is gone.
  • An engine detail tells you how one engine does the work. Example: it uses a helper thread, or it moves objects that are still in use.

This habit saves you from a common debugging mistake. A heap snapshot is a saved picture of everything in memory. It may show a removed button today. It can't promise that the next cleanup runs right after one particular click. In V8, SpiderMonkey, JavaScriptCore, and Hermes, the useful question is the same: which root can reach this object right now?

V8: Orinoco and Oilpan

V8 is the engine in Chrome and Node.js. Its garbage-collection project is called Orinoco. Orinoco shares out the work in three ways: parallel, incremental, and concurrent. You'll see what each word means just below. V8 also has two kinds of cleanup:

  • A quick, frequent cleanup of new objects, called the scavenger. Many new objects are thrown away soon, so checking them often frees memory quickly.
  • A bigger cleanup of all objects. It marks every reachable object, sweeps away the rest, and can compact some pages. Compacting means moving the objects that are left close together.
A value that is only needed for a momentPop out in the code editor (opens in a new tab)JavaScript
function priceWithTax(price) {
  const total = price + 3;
  console.log(total);
}
priceWithTax(10);

Line 5 calls the function with 10. Line 2 makes total. Line 3 prints 13. When the call ends, no code can use total any more. JavaScript doesn't say when V8 will reuse that memory.

A browser is mostly written in C++, another programming language. The parts of a page, like buttons, are called the DOM, and they live in C++ memory. Each DOM button also has a JavaScript wrapper: the object your code holds, which stands for the C++ button. Chrome uses Oilpan, a garbage collector for C++ objects. Together with V8, Oilpan can follow references from JavaScript into C++ and back.

Name two pieces of cleaning workPop out in the code editor (opens in a new tab)JavaScript
const work = ["mark", "sweep"];
console.log(work.join(" then "));

Line 1 makes a small list with two pieces of cleaning work. Line 2 prints mark then sweep. This code doesn't show V8's insides. It only names the work. A thread is a separate line of work that the computer can run next to others. Orinoco can share out the work in three ways:

  • Parallel: JavaScript pauses, and several helper threads do the work at the same time.
  • Concurrent: a helper thread does some of the work while your JavaScript keeps running.
  • Incremental: the main thread does the work in small pieces, between pieces of your code.

None of these choices changes which reachable objects are kept.

A pointer is a memory address that points at an object. Your JavaScript code never sees C++ pointers, but the browser's own code uses them all the time. Following both sides together makes sure neither collector decides one side is garbage while the other side can still reach it.

SpiderMonkey: memory split into zones

SpiderMonkey is the engine in Firefox. Mozilla describes its collector with a long list of words. Here is what each one means:

  • Tracing: it follows references from the roots.
  • Precise: it knows exactly where every reference is, so it never has to guess.
  • Incremental: it can do the work in small pieces.
  • Generational: it sorts objects by age and checks new ones more often.
  • Partly concurrent and parallel: some of the work runs on helper threads.
  • Compacting: it can move objects that are left close together.
A short chain of objectsPop out in the code editor (opens in a new tab)JavaScript
const user = { cart: { item: "tea" } };
console.log(user.cart.item);

Line 1 makes a path: from user to a cart object, then to the string "tea". Line 2 prints tea. A precise collector knows which properties hold references as it follows a path like this.

The heap is the memory where objects live. SpiderMonkey splits its heap into parts called zones. It can clean one zone on its own. Objects in different zones can still point to each other.

Inside a zone, objects sit in arenas: small blocks of heap memory of one fixed size. When SpiderMonkey compacts, it moves objects between arenas. So it must also update every reference to a moved object.

Two zones, as a small modelPop out in the code editor (opens in a new tab)JavaScript
const zones = { account: ["user"], shop: ["cart"] };
console.log(Object.keys(zones).join(","));

Line 1 makes a small model with two named zones. Line 2 prints account,shop. A real zone is not a JavaScript property. It is a split inside the engine. It lets SpiderMonkey clean one zone at a time.

Count the arenas in usePop out in the code editor (opens in a new tab)JavaScript
const arenas = [["user"], [], ["cart"]];
console.log(arenas.filter((arena) => arena.length).length);

Line 1 makes three arenas. The middle one is empty. Line 2 counts the arenas that hold something and prints 2. In a real compacting cleanup, the objects that are left may move, so empty arenas can be given back. SpiderMonkey says compacting updates every reference to moved objects. It also says compacting is not done in small pieces: it runs in one go.

Before: three arenas, user, an empty one, and cart. Two are in use. After compacting: user and cart sit next to each other with no empty gap.before: 3 arenasusercartempty2 in useafter compactingusercartmoved togetherno empty gap
Line 2 counts the arenas that hold something, so it prints 2. Compacting moves user and cart next to each other, so the empty space can be given back.

Split into zones doesn't mean cut off. A user object in one zone can still point to a cart in another zone. A zone is just a smaller piece the collector can clean on its own. The engine keeps notes about references between zones. So a path that crosses zones is never lost during a cleanup.

JavaScriptCore: Riptide and stack scanning

JavaScriptCore is the engine in Safari. The WebKit team built a collector for it called Riptide. Riptide is concurrent: most of its work runs while your JavaScript keeps running. WebKit describes these parts of it:

  • Helper threads share the marking work, in parallel.
  • Write barriers: small checks that run when your code changes a reference, so the collector doesn't miss the change.
  • Short stops at safe points: places in the code where the program can pause safely. Some steps need everything to hold still for a moment.
Two references to one objectPop out in the code editor (opens in a new tab)JavaScript
const cart = { item: "tea" };
const savedCart = cart;
console.log(savedCart.item);

Line 1 makes the cart. Line 2 makes a second reference to the same object. Line 3 prints tea. A real collector must find every path to an object, even while the app keeps changing those paths.

The stack is the memory that holds the local values of functions that are running. Native code is code the computer runs directly: the engine's own C++, or machine code the engine made from your JavaScript. On the stack, native code keeps plain numbers, and the collector can't always tell what a number means. If a number could be the address of an object, Riptide plays safe and treats it as a reference. This is called conservative scanning.

Pin or move? A small modelPop out in the code editor (opens in a new tab)JavaScript
const words = [7, 42, 99];
console.log(words.includes(42) ? "pin" : "move");

Line 1 stores three plain numbers in this model. Line 2 prints pin, because 42 is in the list. Real engines don't check JavaScript values this way. WebKit checks the numbers on the stack against where objects really are in memory. This code only shows the safe choice.

Riptide also never moves objects. To tell old objects from new ones, it keeps a mark on each object between cleanups. It doesn't move the objects that are left into another area. That fits well with conservative scanning. If native code might still hold an object's address, leaving the object where it is is safer than moving it.

Conservative means careful. The collector would rather keep an object a little too long than free or move an object that native code might still use. That can waste some memory. But native code never ends up holding an address that points to nothing, called a dangling pointer, which could crash the program.

Hermes and Hades

Hermes is the JavaScript engine most React Native apps use. Its collector is called Hades. Hermes calls Hades mostly concurrent. It splits objects into two age groups:

  • New objects, the young generation, get quick, frequent cleanups.
  • Older objects, the old generation, get a mark-and-sweep cleanup. Most of that work runs on a background thread. It decides what to keep from the state of memory at the start of the cleanup.

Background work can make long pauses shorter. It still doesn't let your JavaScript know when a cleanup will happen.

A small order object, like in a React Native appPop out in the code editor (opens in a new tab)JavaScript
const order = { item: "tea", count: 3 };
console.log(order.count);

Line 1 makes one object. Line 2 prints 3. Hermes might keep this object with the young objects, move it to the old ones later, or free it. Your JavaScript can't choose.

A short list of new ordersPop out in the code editor (opens in a new tab)JavaScript
const youngOrders = ["tea", "book"];
console.log(youngOrders.length);

Line 1 makes two order names. Think of them as new objects. Line 2 prints 2. A collector that sorts objects by age is called generational. It checks new objects more often, because many of them are thrown away soon. This only makes cleaning faster. It doesn't promise that one order is freed first.

Use the same good habits in a browser or in a mobile app:

  • Remove listeners you no longer need.
  • Clear timers you no longer need.
  • Keep caches small.
  • Measure a real problem with a memory tool before you change how your code makes objects.

"Mostly concurrent" is a reason not to trust one smooth test run. The collector may still need short pauses. It may also behave differently on another phone. Measure on the devices and screens that matter. Then remove whatever holds on to memory by mistake, before you change how your code makes objects.

Tracing JavaScript and C++ together

A button on a web page is not only a JavaScript object. Your code sees the wrapper, the JavaScript object. The browser also keeps its own C++ data for the button. A listener function can hold on to the wrapper. Before the collector frees either part, it must follow the references in both directions.

A button and a listenerPop out in the code editor (opens in a new tab)JavaScript
const button = document.createElement("button");
button.textContent = "Buy";
button.onclick = () => console.log(button.textContent);
console.log(button.textContent);

Line 1 makes the button. Line 2 sets its text. Line 3 stores a listener function that uses button. So the listener holds a reference back to the wrapper. Line 4 prints Buy.

The wrapper also stands for the C++ object that is the real button. Each browser links the two in its own way. But the collector can't treat the JavaScript side and the C++ side as two separate piles. Oilpan, working with V8, is one documented way to follow references across both sides.

Delete one path, but the listener still holds the buttonPop out in the code editor (opens in a new tab)JavaScript
const page = { button: "button" };
const listener = { button: page.button };
delete page.button;
console.log(listener.button);

Line 1 makes a small model of a page. Line 2 copies the button into a listener model. Line 3 deletes the page's property. Line 4 still prints button, because the listener has its own path to it.

A retaining path is the chain of references from a root that keeps an object alive. On a real page, look at the retaining path before you assume that removing a node removed every path to it.

The step-through and the playground below use a small model of a button. Each name stands for one part:

  • window: the root.
  • js-button: the JavaScript wrapper.
  • js-listener: the listener function.
  • cpp-button: the browser's C++ button.
JavaScript side: the root window points to js-button. js-button and js-listener point to each other. C++ side: js-button and cpp-button point to each other across the line.JavaScript sideC++ sidewindowrootjs-buttonjs-listenercpp-button
The trace starts at the root, window. It follows every arrow, even the ones that cross into the browser's C++ side, so all four parts are kept.
Step through a trace of both sides
Step 0 of 6Ready
Your turn: follow the blue line

Replay a small model of tracing a JavaScript button and its C++ DOM object together.

Running in
  1. script
Next: line 1
const graph = {  window: ["js-button"],  "js-button": ["cpp-button", "js-listener"],  "js-listener": ["js-button"],  "cpp-button": ["js-button"],};const result = traceBothHeaps(roots, graph);console.log(result.reachable.join(","));
CallStoreChangeResultBlue line: runs next. 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.
Playground: cut the root's reference
Model of the JavaScript and C++ sidesJavaScript
const roots = ["window"];
const graph = {
  window: ["js-button"],
  "js-button": ["cpp-button", "js-listener"],
  "js-listener": ["js-button"],
  "cpp-button": ["js-button"],
};

console.log(traceBothHeaps(roots, graph).join(","));
What the trace found

Kept: cpp-button, js-button, js-listener, window

Garbage: none

Try it yourself

The root reaches the JavaScript wrapper, the listener, and the C++ button, so all are kept.

A small model. It runs the lesson's tracer, not your browser's garbage collector.
Real-life analogyTwo families sharing one car

Two families share one car. Before they sell it, both families check that nobody still needs it. In the same way, a button and its listener become garbage only when the trace has looked at both sides and found no path from a root.

In real life: One family holds the car key
In JavaScript: A JavaScript root reaches the wrapper
In real life: The other family keeps the car papers
In JavaScript: The C++ button is part of the trace too
In real life: Both families check before selling the car
In JavaScript: References on both sides are followed
In real life: Nobody in either family needs the car
In JavaScript: The removed button and its listener are garbage

Conservative stack scanning

Conservative stack scanning works like this: when a number on the native stack looks like an object's address, the collector treats it as a reference. So it never frees an object that native code still uses. The cost is false retention: a plain number may just happen to look like an address, so an unused object is kept.

Does a stack number look like the address?Pop out in the code editor (opens in a new tab)JavaScript
const objectAddress = 42;
const stackWords = [7, 42, 99];
const pinned = stackWords.includes(objectAddress);
console.log(pinned ? "kept and pinned" : "can move");

Line 1 gives the object a pretend address, 42. Line 2 puts 42 among the stack numbers. Line 3 finds the match. Line 4 prints kept and pinned. This is a model. It doesn't show real addresses in your browser.

Stack words 7, 42, and 99. An arrow goes from 42 to an object whose address is 42, labeled looks like 42. Under the object: kept and pinned.stackWords74299looks like 42address 42objectkept and pinned
The stack holds the number 42, which is also the object's address. The collector can't tell if it is a real pointer, so it keeps the object and doesn't move it.

If the engine can't tell whether a number is really a pointer, moving the object could make that pointer wrong. Keeping the object in place is safer. An object that must stay in place is called pinned. That is why conservative scanning can stop some objects from being compacted.

Step through a conservative stack scan
Step 0 of 4Ready
Your turn: follow the blue line

Replay a small model of a conservative scan. A matching number can keep an object even when it was never a real reference.

Running in
  1. script
Next: line 1
const stackWords = [7, 42, 99];const result = scanConservatively(stackWords, address);console.log(result.state);
CallStoreChangeResultBlue line: runs next. 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.
Playground: change one stack number
Is the address on the stack?JavaScript
const address = 42;
const stack = [7, 42, 99];
console.log(stack.includes(address));
What the model decides
kept and pinned
Try it yourself

42 matches the address, so the model says: kept and pinned.

Each number is only a stand-in for something that might be a pointer. Real engines use their own records of where objects are.

Compare the engines

The names below are useful starting points. They don't promise that every version of an engine works the same way. All four engines may change how they clean, while JavaScript keeps working the same.

Read each row as: this engine, how its collector works, and why that matters to you.

How each engine's garbage collector works
EngineHow its collector worksWhy it matters
V8 / OrinocoHelper threads do most of the work, in parallel or while JavaScript runs. The main thread can also work in small pieces. The big cleanup marks, sweeps, and can compact.In Chrome, Oilpan lets V8 trace C++ objects and JavaScript objects together.
SpiderMonkeyPrecise, incremental, generational, partly concurrent, parallel, and compacting. Its heap is split into zones.It can clean one zone on its own, even when objects point across zones.
JavaScriptCore / RiptideConcurrent, generational, and parallel. It scans the native stack conservatively and never moves objects.A number that might be a pointer keeps its object where it is.
Hermes / HadesMostly concurrent. New objects get quick cleanups. Old objects get mark-and-sweep, mostly on a background thread.Background work makes pauses shorter, but it is still the engine's choice, not a promise.

The table only gives the big picture. It tells you which engine a memory report or an article is about. It doesn't let your code count on a certain pause, address, or cleanup time.

What web developers should do

Different collectors don't change the first question to ask when you debug memory: which root still reaches this object? A removed button with a listener can stay in memory because your code, the browser, or a cache still points to it.

Language rule or engine detail?
  • Keep an object that a root can reach.
  • Move the objects that are left close together, to remove gaps.
  • Clean one part of the heap at a time.
  • Visit a JavaScript wrapper, then its C++ DOM object.
  • A matching stack number keeps address 42 alive.
  • Remove an unused listener that keeps data alive.
Try it yourself
0 of 6 correct

Put each sentence in the right group. Read the explanation after each choice.

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

Start with the memory tools in the browser or in React Native. Look at the retaining paths, remove references you didn't mean to keep, and then measure again. A WeakRef holds an object without keeping it alive. Don't add a WeakRef just to hide a problem you don't understand yet.

Common misconceptions

  • "All engines clean memory the same way." They all follow the same JavaScript rules. But each engine cleans in its own way.
  • "A button removed from the page is freed at once." A listener, a wrapper, a cache, or a browser reference can still hold it.
  • "A number that matches an address proves the object is in use." It is only a safe guess. It can keep memory that nobody needs.
  • "Forcing a cleanup fixes a leak." A leak is usually a path you didn't mean to keep. A cleanup can't free anything that path still reaches.

Each row below names a term, says what it means, and says what it does not mean.

Terms that are easy to mix up
TermWhat it meansWhat it does not mean
Same JavaScript rulesEvery engine keeps what a root can reach, and your code sees the same JavaScript behavior.Every engine cleans memory in the same steps.
Conservative rootA stack number that might be a pointer, so the collector keeps its object to be safe.Proof that the number really was a reference to the object.
DOM wrapperThe JavaScript object that stands for the browser's C++ DOM object.A separate object that can be freed without looking at the C++ side.

Practice exercises

Exercise 1 · Warm-upCount the references in a tiny graph

Predict what this tiny graph prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const graph = { root: ["js"], js: ["cpp"], cpp: [] };
console.log(graph.root.length + graph.js.length + graph.cpp.length);

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

    Exercise 2 · Warm-upPinned or movable?

    What does this stack model print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const words = [3, 42];
    console.log(words.includes(42) ? "pinned" : "movable");

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

      Exercise 3 · PracticeName V8's project

      Name the V8 garbage-collection project from this lesson.

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

        Exercise 4 · PracticeName SpiderMonkey's memory parts

        What does SpiderMonkey call the parts of its heap that it can clean on their own?

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

          Exercise 5 · ChallengeFree a removed button

          A checkout button was removed from the page. It still has a listener that uses a large order object. What should you remove?

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

            Exercise 6 · ChallengeApply it to a real site

            Your app uses more memory after each page change. What should you do before you rewrite the component?

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

              Check your understanding

              Start with the shared rule: what can a root reach? Then name the engine feature or browser link that changes how the collector follows that rule.

              GC across engines quiz · 8 questionsScore: first tries count
              1. Question 1 of 8This model lists the references from a JavaScript wrapper. What does it print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const js = ["cpp"];
                console.log(js.length);

                Choose an answer to see the explanation.

              2. Question 2 of 8What is Orinoco in this lesson?

                Choose an answer to see the explanation.

              3. Question 3 of 8Why can a conservative scan keep extra memory?

                Choose an answer to see the explanation.

              4. Question 4 of 8Which SpiderMonkey feature lets it clean parts of the heap on their own?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const words = [7, 42];
                console.log(words.includes(42) ? "pinned" : "movable");

                Choose an answer to see the explanation.

              6. Question 6 of 8Why must a DOM wrapper and its C++ object be traced together?

                Choose an answer to see the explanation.

              7. Question 7 of 8What does this zone example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const zones = { account: ["user"], shop: ["cart"] };
                console.log(zones.shop[0]);

                Choose an answer to see the explanation.

              8. Question 8 of 8A number on the stack matches an object's address. What is the safe conclusion?

                Choose an answer to see the explanation.

              Key takeaways

              • JavaScript sets the rules your code can see. Each engine chooses how to clean memory.
              • Orinoco (V8), SpiderMonkey's zones, Riptide (JavaScriptCore), and Hades (Hermes) are four different documented designs.
              • A DOM button has a JavaScript wrapper and C++ data, so a full trace follows both sides.
              • Conservative scanning picks safety over certainty, and it can pin objects in place.
              • For a real leak, look at the retaining paths and remove references you didn't mean to keep.

              Remember the one-liner.
              Engines clean memory in different ways, but every safe trace keeps everything a root can reach.

              Coming next: Heap snapshots in depth, where you read retaining paths and object sizes in a memory tool.

              CompleteFrontend Clear concepts. Working examples.