completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Incremental, concurrent & parallel GC

Learn how JavaScript engines split, overlap, and share garbage collection work to keep pages responsive while memory is reclaimed.

By the end, you can
  • 01
    Follow the three marking colorsExplain what white, grey, and black objects mean, and why a write barrier turns a new white object grey.
  • 02
    Tell the plans apartTell apart small slices, a helper working beside your code, several helpers at once, safepoints, and idle-time cleaning.
  • 03
    Read engine claims carefullySay what V8's Orinoco project changed, without promising a fixed pause time or schedule.

Cleaning memory without freezing the page

The garbage collector is the part of the JavaScript engine that frees memory. It finds the objects your program can still reach and throws the rest away. You met it in the last few lessons.

To find what is still reachable, the collector must look at many objects. That takes time.

The simplest plan is to stop JavaScript and do all the work in one go. This is called a stop-the-world pause. While JavaScript is stopped, the page cannot react to typing, scrolling, or clicks. A long pause makes the page feel frozen.

So modern engines split the same work into smaller pieces, or let helpers do it at the same time as your code.

Definition

Incremental, concurrent, and parallel garbage collection are three ways to plan the collector's work. Incremental: do it in small slices, with your code running in between. Concurrent: a helper does it while your code keeps running. Parallel: several helpers share one piece of the work.

Two words you will see a lot:

  • The main thread is the one line of work where your JavaScript runs. The page also answers clicks and draws itself there.
  • A helper thread, or just a helper, is a second worker. It runs at the same time, on another core of the processor.

These plans change when and where the work runs. They do not change which objects stay. The collector still starts from the roots (starting points such as global variables), follows every reference, and keeps every object it reaches.

This lesson uses small JavaScript models. They show each plan in a few lines. They do not look inside your real browser engine.

The table compares each plan to cleaning a house. Read across: the plan's name, the house version, then what your page feels.

Four ways to plan the same cleaning job
PlanCleaning a houseWhat your page feels
Stop-the-worldYou clean the whole house while nobody may use it.One long pause.
IncrementalYou clean for ten minutes each evening.Several short pauses.
ConcurrentA helper cleans while you keep living there.Your code keeps running while the helper works.
ParallelSeveral friends clean different rooms at the same time.One piece of work finishes sooner.

Three colors for marking

Marking is the collector's search for live objects. It starts from the roots and follows each reference to the next object.

Tri-color marking ("three-color marking") keeps track of that search with three colors:

  • White: not seen yet.
  • Grey: found, but the collector has not yet looked at the objects it points to.
  • Black: found, and every object it points to has been looked at. This object is done.

When marking ends, every object that is still white was never reached. It is garbage.

Three colors in a tiny modelPop out in the code editor (opens in a new tab)JavaScript
const graph = {  user: ["cart"],  cart: ["items"],  items: [],};const colors = { user: "grey", cart: "white", items: "white" }; function markOne(name) {  for (const child of graph[name]) colors[child] = "grey";  colors[name] = "black";  console.log(name, colors.user, colors.cart, colors.items);} markOne("user");markOne("cart");markOne("items");

Lines 1 to 5 build a small chain: user points to cart, and cart points to items.

Line 6 makes user grey, because the collector has found it. The other two are white.

The function markOne checks one object. In plain words, line by line:

  1. Line 9 makes each object it points to grey.
  2. Line 10 makes the object itself black.
  3. Line 11 prints the three colors.

Lines 14 to 16 check the objects in order. The three logs are user black grey white, then cart black black grey, then items black black black.

After markOne of user. Three cards joined by arrows: user points to cart, cart points to items. User is black, fully checked. Cart is grey, found but not checked. Items is white, not seen yet.markOne("user")userblackfully checkedcartgreyfound, not checkeditemswhitenot seen yet
Line 14 checks user. It finds cart and makes it grey, then makes user black. Nobody has reached items yet, so the first log is user black grey white.
Real-life analogyA checklist for a room

Picture checking a room with a list. A cupboard you have not looked at yet is white. A cupboard you found but have not opened is grey. A cupboard whose drawers you have all checked is black. You keep opening grey cupboards until none are left.

In real life: A cupboard you have not looked at yet
In JavaScript: White object
In real life: A cupboard you found, with its drawers still shut
In JavaScript: Grey object
In real life: A cupboard with every drawer checked
In JavaScript: Black object

Each row of the table is one color. Read across: what it means, then why the collector cares.

What each marking color means
ColorWhat it meansWhy it matters
WhiteNot seen yet in this round of marking.If it is still white at the end, it is garbage.
GreyFound, but the objects it points to are not all checked.It waits in a to-do pile for the collector.
BlackFound, and everything it points to is checked.It must never point to a white object that nobody will check.
Step through the three colors
Step 0 of 8Ready
Your turn: follow the blue line

Step through the three-color model. Each step calls the lesson's real markOne function and saves the colors it returns.

Running in
  1. script
Next: line 1
  user: ["cart"],  cart: ["items"],  items: [],};const colors = { user: "grey", cart: "white", items: "white" }; function markOne(name) {  for (const child of graph[name]) colors[child] = "grey";  colors[name] = "black";  console.log(name, colors.user, colors.cart, colors.items);} markOne("user");markOne("cart");markOne("items");
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.

The step-through calls the same markOne function as the code above. It saves the colors after every step, so you can step back. It replays the model. It is not a debugger inside a real engine.

Incremental marking: small slices

Incremental marking splits one marking job into short slices. The main thread marks a few objects, goes back to running your code, then marks a few more later.

Each pause is shorter, so the page stays responsive. The cost is some extra record keeping, because the collector must remember where it stopped.

Four objects, two per slicePop out in the code editor (opens in a new tab)JavaScript
const work = ["user", "cart", "items", "price"];const sliceSize = 2;console.log(Math.ceil(work.length / sliceSize));

Line 1 lists four objects to mark. Line 2 says to mark two objects in each slice. Line 3 prints 2: four objects in slices of two make two slices. Math.ceil rounds up, so a leftover object would still get its own slice.

Between the two slices, the page can do other work. In a real engine, some objects take longer to mark than others, so slices are not this even.

The playground below uses six objects. The picture shows them marked all at once, and in slices of two.

Top: all at once, one long card with all six objects, one pause of 6 objects. Bottom: in slices of 2, three cards, user and cart, items and price, coupon and address, with app runs between them. Pauses 1, 2 and 3.all at onceuser cart items price coupon address1 pause of 6 objectsin slices of 2user, cartpause 1items, pricepause 2coupon, addresspause 3app runsapp runs
The same six objects get marked both ways. With markInSlices(2) there are 3 pauses of 2 objects, and the app runs between them. So line 15 prints 3 2.
Playground: how many objects per pause?
Marking six objects in slicesJavaScript
const work = ["user", "cart", "items", "price", "coupon", "address"]; function markInSlices(sliceSize) {  let pauses = 0;  let longestPause = 0;  for (let start = 0; start < work.length; start += sliceSize) {    const slice = work.slice(start, start + sliceSize);    pauses += 1;    longestPause = Math.max(longestPause, slice.length);    // The app can run between these slices.  }  return { pauses, longestPause };} console.log(markInSlices(2).pauses, markInSlices(2).longestPause);
Pauses in the model3 pauses
pause 12 objects

user, cart

Your code can run after this pause.

pause 22 objects

items, price

Your code can run after this pause.

pause 32 objects

coupon, address

Your code can run after this pause.

Try it yourself

With 2 objects per pause, the model makes 3 pauses. The longest pause marks 2 objects.

This is a small model. A real engine does not count objects; it gives each slice a time limit and picks when slices run.

Change only the number of objects per pause. With 1, you get six pauses, and each one is as short as it can be. With 6, you get one pause that holds all six objects.

A real engine does not count objects like this. It gives each slice a time limit, and it also watches how fast your code makes new objects.

Concurrent marking: a helper works beside your code

Concurrent marking lets a helper thread mark objects while your JavaScript keeps running on the main thread. The main thread has less to do.

But your code can change references while the helper is looking. Say the helper has already finished the cart, so the cart is black. Then your code puts a white coupon into the cart. Nobody looks inside a black object again, so the coupon would never be checked. It could be thrown away while it is still in use.

A write barrier fixes this. You met write barriers in the last lesson: a small piece of engine code that runs every time your code stores a reference into an object. Here, the barrier turns the new white object grey. Grey objects are always checked before marking ends.

A barrier turns the new coupon greyPop out in the code editor (opens in a new tab)JavaScript
const colors = { cart: "black", coupon: "white" }; function storeCartCoupon() {  // The app just put the white coupon into the black cart.  colors.coupon = "grey";  console.log(colors.cart, colors.coupon);} storeCartCoupon();

Line 1 starts with a cart that is black (done) and a coupon that is white (not seen). The comment on line 4 says what just happened: the app put the white coupon into the black cart. Line 5 is the barrier: it turns the coupon grey. Line 6 prints black grey.

Left: the cart, which is black and already checked, now holds the coupon. An arrow labeled barrier goes to a box called to check, which holds the coupon. The coupon is now grey.couponcartcart: blackbarriercouponto checkcoupon: grey
The app has just put the white coupon into the black cart. Line 5 is the barrier: it turns the coupon grey, so the marker will still check it. Line 6 prints black grey.
Step through the barrier
Step 0 of 4Ready
Your turn: follow the blue line

Step through the barrier model. Real engines use faster barriers, but the rule is the same: a white object put into a black one turns grey.

Running in
  1. script
Next: line 1
 function storeCartCoupon() {  // The app just put the white coupon into the black cart.  colors.coupon = "grey";  console.log(colors.cart, colors.coupon);} storeCartCoupon();
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.

Some objects are hard or unsafe for a helper to finish alone. One example is a host object: an object the browser provides, such as a page element.

A bailout worklist is a to-do list for those objects. The helper puts them on the list and moves on. Later, the main thread handles them at a safe moment. This is not a failure. It keeps the helper's job simple and correct.

The helper leaves hard work for the main threadPop out in the code editor (opens in a new tab)JavaScript
const helperPile = ["cart", "items"];const bailoutWorklist = ["hostObject"];const mainThreadPile = []; while (bailoutWorklist.length) {  mainThreadPile.push(bailoutWorklist.shift());} console.log(helperPile.length, mainThreadPile.join(","));

Line 1 is the helper's pile of ordinary objects. Line 2 is the bailout worklist, with one hard object. Lines 5 to 7 move that object to the main thread's pile. shift() takes the first item off a list.

Line 9 prints 2 hostObject. The helper still has two ordinary objects, and the main thread has the hard one.

To-do piles and work stealing

A marking worklist is the to-do pile of grey objects that still need checking. When several helpers work together, each one keeps its own pile. That way a helper can take its next object without asking the others first.

Moving work to an empty helperPop out in the code editor (opens in a new tab)JavaScript
const helperOne = ["user", "cart", "items"];const helperTwo = [];helperTwo.push(...helperOne.splice(1));console.log(helperOne.join(","), helperTwo.join(","));

Line 1 gives the first helper three grey objects. Line 2 gives the second helper an empty pile. Line 3 moves two objects to the empty pile:

  • splice(1) cuts out every item from position 1 onward.
  • push(...) adds those items to the other pile.

Line 4 prints user cart,items.

Box helperOne keeps user. An arrow labeled takes 2 goes to box helperTwo, which was empty and now holds cart and items.userhelperOnekeeps 1takes 2cart, itemshelperTwowas empty
helperTwo starts with nothing to do, so it takes cart and items from helperOne. Line 4 prints user cart,items.

Work stealing is this balancing step. A helper that runs out of work takes some from a fuller pile. Exactly how much it takes is up to the engine. What matters is that no helper sits idle while others are busy.

Work stealing only moves the collector's own to-do items between helpers. Your JavaScript does not move between threads.

Which plan is it?
  • Mark two objects, let the app run, then mark two more.
  • Several short pauses instead of one long pause.
  • A helper marks ordinary objects while JavaScript keeps running.
  • A helper leaves a hard object for the main thread.
  • Two helpers copy young objects to a new place.
  • An idle helper takes half of a busy helper's grey pile.
Try it yourself
0 of 6 correct

Sort each card by the plan it describes: incremental, concurrent, or parallel.

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

Safepoints: planned places to pause

A safepoint is a place in running code where the JavaScript thread can stop and the engine can read its state safely. Think of a bus. It stops at bus stops, not in the middle of the road.

Sometimes the collector needs JavaScript to stop for a short step that cannot run at the same time as your code. Then it waits until your code reaches the next safepoint.

Pause only at a bus stopPop out in the code editor (opens in a new tab)JavaScript
const steps = ["run handler", "bus stop", "run handler", "bus stop"];let paused = false; for (const step of steps) {  if (step === "bus stop") paused = true;} console.log(paused ? "paused safely" : "keep running");

Line 1 lists four steps. Two of them are "bus stop". Line 5 sets paused to true only at a bus stop. Line 8 prints paused safely.

Real safepoints sit in the engine's machine code, not in a list like this. The model only shows the idea of a planned stop.

Safepoints are why "concurrent" does not mean "no pauses". Helpers can do most of the work beside your code. But a few short pauses are still needed, for example to finish a step or to move objects safely. The win is shorter pauses on the main thread, not zero pauses.

Parallel copying and moving

Some collector steps can be shared by several helpers at once:

  • Parallel scavenging. Scavenging is the quick clean of young memory: it copies the young objects that are still in use to a new place. In parallel scavenging, several helpers do that copying together.
  • Parallel compaction. Compaction moves live objects close together, so the free memory is in one big piece. In parallel compaction, several helpers move objects and fix the references that point to them.

Both use several cores of the processor, so one collector step finishes sooner.

Split the copying between two helpersPop out in the code editor (opens in a new tab)JavaScript
const youngSpace = ["cart", "items", "coupon", "address"];const helpers = [[], []]; youngSpace.forEach((name, index) => helpers[index % 2].push(name)); console.log(helpers[0].join(","), helpers[1].join(","));

Line 1 lists four young objects. Line 2 makes two empty piles. Line 4 deals the names out like playing cards. index % 2 is the remainder after dividing by 2, so objects 0 and 2 go to the first pile, and objects 1 and 3 go to the second. Line 6 prints cart,coupon items,address.

The model only splits names. Real copying also leaves a note at the old place saying where the object went, and updates every reference to it.

Moving objects needs care, because your code must never use an old memory address. Engines use their own records, barriers, and safepoints to make the move invisible. After compaction, your variable still points to the same object, even though the object now sits somewhere else in memory.

Idle-time cleaning and Orinoco

The browser draws the page as a series of pictures called frames. To look smooth, it draws about 60 frames a second, so each frame has about 16 milliseconds (ms).

Idle-time GC uses the spare time left in a frame, when the browser has some. It is like tidying up during an ad break on TV: it only works if there is a break. Typing, drawing, and network work can use up all the spare time. So idle time is a chance, not a promise.

Is there enough spare time?Pop out in the code editor (opens in a new tab)JavaScript
const frame = { workMs: 11, budgetMs: 16 };const spareMs = frame.budgetMs - frame.workMs;console.log(spareMs >= 3 ? "run idle GC" : "wait");

Line 1 says the frame has 16 ms and the page used 11 ms. Line 2 works out 5 ms of spare time. Line 3 prints run idle GC, because 5 is at least the model's minimum of 3 ms. A busier frame would print wait.

V8 is the JavaScript engine inside Chrome and Node. Orinoco is the name of the V8 project that moved most of its collector work off the main thread. It made the collector mostly parallel and concurrent.

Orinoco is not something your JavaScript can call. It also does not promise the same behavior on every page. It names a direction: move more cleaning work off the main thread, whenever that stays correct.

What a log can and cannot tell you

If you start Node with --trace-gc, it prints a line each time the collector runs. A full collection shows up as Mark-Compact. But whether incremental marking starts depends on timing, so one log does not tell you how the next run will go. That is why this lesson uses models instead.

What this means for your page

Your code does not choose how the collector plans its work. But it can make the collector's job easier or harder:

  • Making lots of objects in a short burst gives it more work.
  • Keeping data you no longer need keeps that data alive.
  • Doing heavy work while the user types, or while an animation runs, leaves no spare time.

When a page stutters, measure first. Do not guess from a GC word in a log.

Leave room in each framePop out in the code editor (opens in a new tab)JavaScript
const frameBudget = 16;const appWork = 6;console.log(appWork < frameBudget ? "leave room" : "too busy");

Line 1 sets the frame budget to 16 ms. Line 2 says the app's work takes 6 ms. Line 3 prints leave room. This does not mean the collector gets the other 10 ms. Drawing, input, and cleaning all share what is left.

When scrolling or typing janks (stutters), record a performance trace in your browser's developer tools. A performance trace is a recording of everything the page did over a few seconds.

When memory keeps growing, use the memory tools instead. Remove things that keep objects alive by accident, such as event listeners you no longer need and caches that grow forever.

The next lesson, Weak references & ephemerons, explains special references that do not keep objects alive. Do not use them in place of clear rules about which code owns which data.

Common misconceptions

  • "Concurrent means no pause." Helpers work beside your code, but safepoints and finishing steps can still pause JavaScript for a short time.
  • "Parallel means my callbacks run at the same time." It means several collector helpers share one step. Your JavaScript still runs one thing at a time.
  • "Grey means garbage." Grey means found but not finished. It is exactly the work the collector still has to check. Objects still white at the end are the garbage.
  • "Idle time is guaranteed." There is spare time only when the browser has some left over.
  • "One log shows the exact schedule." When the collector runs depends on how fast your code makes objects, on the device, and on the engine version.

Each row of the table is one word that is easy to mix up. Read across: what it means, then what it does not mean.

Words that sound alike but mean different things
WordWhat it meansWhat it does not mean
ConcurrentSome collector work runs at the same time as your JavaScript.JavaScript never pauses.
ParallelSeveral collector helpers do one step together.Your own JavaScript code runs on several threads.
IncrementalOne job is split into small slices.The collector needs no extra record keeping or barriers.
Idle-time GCThe work uses spare time when the browser has some.The browser promises a fixed amount of spare time.

All four plans aim for a page that stays responsive. Each one solves a different problem:

  • Incremental keeps each pause short.
  • Concurrent lets the work happen beside your code.
  • Parallel uses more helpers, so one step finishes sooner.
  • Idle-time uses spare time between frames.

Real engines combine them. They do not pick just one.

Practice exercises

Exercise 1 · Warm-upPredict the colors

Read the small model. Type the two colors it prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const colors = { user: "grey", cart: "white" };
colors.cart = "grey";
colors.user = "black";
console.log(colors.user, colors.cart);

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

    Exercise 2 · Warm-upCount the slices

    Predict how many pauses this model prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const work = ["user", "cart", "items", "price"];
    const size = 2;
    console.log(Math.ceil(work.length / size));

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

      Exercise 3 · PracticeProtect the new coupon

      The app just put a white coupon into a black cart. What color should the barrier give the coupon?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const colors = { cart: "black", coupon: "white" };
      if (colors.cart === "black" && colors.coupon === "white") colors.coupon = "grey";
      console.log(colors.coupon);

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

        Exercise 4 · PracticeName the list for hard objects

        A helper cannot finish marking a host object (an object the browser provides). Name the list it should put the object on.

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

          Exercise 5 · ChallengeChoose the safe stop

          In this lesson's bus model, where can the JavaScript thread pause safely?

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

            Exercise 6 · ChallengeApply this to a real page

            A search box on a real website sometimes stutters while you type. What should you record before you blame garbage collection?

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

              Check your understanding

              For each question, first decide what it is about: a color, a slice, a helper, or a planned place to pause.

              Incremental and concurrent GC quiz · 7 questionsScore: first tries count
              1. Question 1 of 7In three-color marking, what does grey mean?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this slice model print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const work = ["user", "cart", "items", "price"];
                const size = 2;
                console.log(Math.ceil(work.length / size));

                Choose an answer to see the explanation.

              3. Question 3 of 7Your code puts a white object into a black one. Why does the barrier turn the white object grey?

                Choose an answer to see the explanation.

              4. Question 4 of 7What is a bailout worklist for?

                Choose an answer to see the explanation.

              5. Question 5 of 7What problem does work stealing solve?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why does a collector use a safepoint?

                Choose an answer to see the explanation.

              7. Question 7 of 7What is Orinoco in V8?

                Choose an answer to see the explanation.

              Key takeaways

              • White means not seen yet. Grey means found but not finished. Black means fully checked.
              • Incremental marking splits the work into slices. Smaller slices make the longest pause shorter.
              • Concurrent helpers need write barriers. They can leave hard objects on a bailout worklist for the main thread.
              • Parallel helpers share work through to-do piles and work stealing. Safepoints are planned places where JavaScript can pause for a short step.
              • Idle-time GC uses spare time in a frame. Orinoco is the V8 project that made its collector mostly parallel and concurrent.

              Remember the one-liner.
              Modern garbage collectors keep exactly the same objects alive. They only plan when and where the work happens.

              Coming next: Weak references & ephemerons.

              CompleteFrontend Clear concepts. Working examples.