Incremental, concurrent & parallel GC
Learn how JavaScript engines split, overlap, and share garbage collection work to keep pages responsive while memory is reclaimed.
- 01Trace tri-color markingExplain white, grey, and black objects and why a write barrier protects the marking rule.
- 02Compare scheduling choicesSeparate incremental slices, concurrent helper work, parallel work, safepoints, and idle-time work.
- 03Read engine claims carefullyDescribe what Orinoco changed without promising a particular pause time or collection schedule.
Keeping collection responsive
A tracing collector must eventually inspect a large graph of objects. Doing every part in one long stop-the-world pause is simple, but a page cannot respond to typing, scrolling, or clicks while JavaScript is stopped. Modern engines therefore schedule the same basic work in smaller or overlapping pieces.
Incremental, concurrent, and parallel garbage collection are ways to schedule collection work: slices over time, helper work beside the app, and several helpers in one phase.
These words describe when and where collector work runs. They do not change JavaScript reachability. The engine still starts from roots, follows references, and preserves objects the program can reach. This lesson uses small models so the scheduling is visible without pretending to inspect your browser engine.
| Word | House cleaning | What changes |
|---|---|---|
| Stop-the-world | Clean the whole house while nobody uses it. | One big pause. |
| Incremental | Clean for ten minutes each evening. | Several short pauses. |
| Concurrent | A helper cleans while you keep living there. | Work overlaps with the app. |
| Parallel | Several friends clean different rooms together. | Several workers share one phase. |
Tri-color marking
Tri-color marking is a checklist model for a tracing collection. White means not looked at yet. Grey means found, but its references have not all been opened. Black means the object and its outgoing references have been fully checked for this marking cycle.
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 through 5 make user -> cart -> items. Line 6 starts user as grey because the marker has found it. Line 9 greys each child, and line 10 blacks the object after its drawer has been opened. The three logs print user black grey white, then cart black black grey, then items black black black.
Think of checking a room with a checklist. A name not looked at is white. A cupboard you found but have not opened is grey. A cupboard whose drawers are all checked is black.
- In real life: Not checked on the list
- In JavaScript: White object
- In real life: Found, but its drawers remain closed
- In JavaScript: Grey object
- In real life: Every drawer has been checked
- In JavaScript: Black object
Where the analogy stops: A real collector does not use literal paper colors. The colors are a compact way to describe its unfinished work.
| Color | Meaning | Why it matters |
|---|---|---|
| White | Not looked at yet in this marking cycle. | It may be garbage unless marking reaches it. |
| Grey | Found, but its outgoing references are not all checked. | It waits on a marking worklist. |
| Black | Fully checked in this marking cycle. | It must not hide a white object from the marker. |
Step through a small tri-color teaching model. It records real calls to the lesson function and its immutable color snapshots.
script
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");The replay calls the same markOne function as the source. It records each color snapshot so you can step backward without seeing a later value early. It is a guided replay of a teaching model, not a debugger paused inside V8.
Incremental marking
Incremental marking splits one marking job into short slices. The main JavaScript thread marks some objects, returns to app work, then marks more later. Smaller slices can reduce the longest visible pause, although the collector needs extra bookkeeping between slices.
const work = ["user", "cart", "items", "price"];const sliceSize = 2;console.log(Math.ceil(work.length / sliceSize));Line 1 names four pieces of work. Line 2 chooses two objects per pause. Line 3 prints 2, because four objects need two slices of two. Between those two model pauses, the page can handle other work. The engine does not promise that every real object costs the same amount to mark.
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);2 objectsuser, cart
The app can run after this slice.
2 objectsitems, price
The app can run after this slice.
2 objectscoupon, address
The app can run after this slice.
With 2 objects per slice, this model has 3 pauses and a longest pause of 2 objects.
Change only the slice size. A size of one produces more pauses but the shortest modeled pause. A size of six produces one pause for this six-object list. In a real engine, a scheduler uses time budgets and allocation pressure instead of simply counting object names.
Concurrent marking and bailout work
Concurrent marking lets a helper thread trace ordinary objects while the JavaScript thread continues running. That overlap can reduce main-thread work, but the app may change references while the helper is looking. This is why the write-barrier idea from the previous lesson returns here.
const colors = { cart: "black", coupon: "white" }; function storeCartCoupon() { // The app stores a white object into a black object. colors.coupon = "grey"; console.log(colors.cart, colors.coupon);} storeCartCoupon();Line 1 starts with a fully checked black cart and an unseen white coupon. Line 5 models an app write. The barrier on line 5 turns the coupon grey, and line 6 prints black grey. Without that repair, a black object could hide a live white child from the marker.
Replay the write-barrier teaching model. Real engines use optimized barriers, but the rule here is intentionally small.
script
function storeCartCoupon() { // The app stores a white object into a black object. colors.coupon = "grey"; console.log(colors.cart, colors.coupon);} storeCartCoupon();Some things are awkward or unsafe for a helper to finish alone. A bailout worklist is a list of those objects: the helper records them, then the main thread handles them at an appropriate safe point. It is not a failure; it is a way to keep concurrent work simple and correct.
const helperPile = ["cart", "items"];const bailoutWorklist = ["hostObject"];const mainThreadPile = []; while (bailoutWorklist.length) { mainThreadPile.push(bailoutWorklist.shift());} console.log(helperPile.length, mainThreadPile.join(","));Lines 1 and 2 separate normal helper work from one hard object. Lines 5 through 7 move that object to the main-thread pile. Line 9 prints 2 hostObject: two ordinary objects remain in the helper pile and the main thread receives the bailout item.
Worklists and work stealing
A marking worklist is the pile of grey objects that still need checking. With several helpers, each can keep its own pile. Local piles avoid constant coordination, because a helper can take its next object without asking every other helper first.
const helperOne = ["user", "cart", "items"];const helperTwo = [];helperTwo.push(...helperOne.splice(1));console.log(helperOne.join(","), helperTwo.join(","));Line 1 starts one helper with three grey objects. Line 3 moves two into the empty helper's pile. Line 4 prints user cart,items. The precise split is an engine choice; the important point is that idle helpers can become useful again.
Work stealing is that balancing step. A helper that finishes early takes some work from a fuller pile. It does not mean user JavaScript moves between threads. It is collector workers sharing collector bookkeeping during a parallel phase.
- 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 difficult object for the main thread.
- Two workers copy young objects into new space.
- An idle worker takes half a busy worker's grey pile.
Sort each card by the scheduling idea it describes. The cards use the same names as this lesson.
Safepoints
A safepoint is a place where the JavaScript thread can pause and the engine can understand its live state. Think of a bus: it stops at bus stops, not in the middle of the road. A collector sometimes needs this cooperation for a short phase that cannot safely overlap arbitrary execution.
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 includes two planned bus stop entries. Line 5 changes paused only at those entries. Line 8 prints paused safely. This tiny model does not represent machine instructions; it shows why an engine chooses coordination points.
Safepoints explain why “concurrent” is not the same as “zero pauses.” Helpers can do a lot beside the app, while short coordinated pauses still establish a stable view, finish a phase, or move objects safely. The payoff is smaller main-thread interruptions, not an impossible promise of none.
Parallel scavenging and compaction
Parallel scavenging lets several workers copy young live objects at the same time. Parallel compaction lets several workers move objects and update references during a compacting phase. These phases use multiple cores to finish one collector job sooner.
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 4 puts alternate names in two helper piles. Line 6 prints cart,coupon items,address. The model only divides names; real copying also installs forwarding information and updates every reference to moved objects.
Moving memory needs coordination because app code must not keep an old address. Engines use metadata, barriers, and safepoints to make the move invisible to JavaScript. Your variable still points at the same logical object after compaction, even though the engine changed where that object sits in memory.
Idle-time GC and Orinoco
Idle-time GC uses spare time between frames when the browser offers it. Like cleaning during a TV ad break, it is useful only when there is time left. Input, rendering, and network work can remove that spare time, so it is an opportunity rather than a deadline.
const frame = { workMs: 11, budgetMs: 16 };const spareMs = frame.budgetMs - frame.workMs;console.log(spareMs >= 3 ? "run idle GC" : "wait");Line 1 gives a frame 16 ms and says the page used 11 ms. Line 2 calculates 5 ms spare time. Line 3 prints run idle GC because five is at least the model's three-millisecond minimum. A busy frame would print wait instead.
Orinoco is V8's project name for work that made its collector mostly parallel and concurrent. It is not a JavaScript API and it does not guarantee one fixed behavior for every page. It names an engineering direction: move more collection work off the main thread when correctness allows.
Node can print Mark-Compact with --trace-gc, but whether incremental marking starts is scheduling-dependent. This lesson keeps its main claims model-based and does not turn one trace into a timing promise.
What this means for a page
Most application code does not choose a collector mode. It can still make the collector's job easier or harder by allocating large bursts, retaining unnecessary data, or doing heavy work during input and animation. The first move is to measure the page, not to guess from a GC term in a log.
const frameBudget = 16;const appWork = 6;console.log(appWork < frameBudget ? "leave room" : "too busy");Line 1 names a simple frame budget. Line 2 represents six milliseconds of app work. Line 3 prints leave room. This is not a rule that GC gets the other ten milliseconds; it is a reminder that rendering, input, and collection all compete for responsiveness.
Use a performance trace when scrolling or typing janks. Use heap tools when memory grows. Remove accidental roots such as unused listeners and unbounded caches. The next lesson, Weak references & ephemerons, explains special weak edges; do not use them as a substitute for clear ownership.
Common misconceptions
- “Concurrent means no pause.” Helpers overlap work, but safepoints and finishing work can still pause JavaScript briefly.
- “Parallel means my callbacks run at once.” It means collector workers share a GC phase, not that ordinary JavaScript loses its single-threaded rules.
- “Grey means garbage.” Grey means discovered but unfinished; it is exactly the work that must be checked.
- “Idle time is guaranteed.” It is spare time only when the browser has it.
- “A trace proves the exact schedule.” Collection timing changes with allocation, device, and engine version.
| Term | What it means | Do not confuse it with |
|---|---|---|
| Concurrent | Some GC work runs alongside JavaScript. | JavaScript never needs brief pauses or safepoints. |
| Parallel | Several GC workers perform one phase together. | The JavaScript language suddenly runs user code in parallel. |
| Incremental | One job is split into small slices. | The collector has no overhead or needs no barriers. |
| Idle-time GC | Work uses spare time when the browser offers it. | The browser promises a fixed amount of idle time. |
The shared goal is responsiveness, but each tool solves a different constraint. Incremental work limits one pause, concurrent work overlaps the app, parallel work uses more workers, and idle-time work uses spare scheduling gaps. Engines combine them rather than selecting exactly one.
Practice exercises
Read the small model and type the two printed colors.
const colors = { user: "grey", cart: "white" };
colors.cart = "grey";
colors.user = "black";
console.log(colors.user, colors.cart);It prints black grey. The user has been fully checked, while cart was just discovered and still needs checking.
Predict the number of modeled marking pauses.
const work = ["user", "cart", "items", "price"];
const size = 2;
console.log(Math.ceil(work.length / size));It prints 2. Two pauses of two objects cover four objects.
What color should the write barrier give the coupon?
const colors = { cart: "black", coupon: "white" };
if (colors.cart === "black" && colors.coupon === "white") colors.coupon = "grey";
console.log(colors.coupon);The answer is grey or gray. Grey means the marker must check the coupon before finishing.
A helper cannot finish marking a host object. Name the list it should use.
Put it on the bailout worklist. The main thread handles that work at a safe time.
Where can the JavaScript thread pause safely in this lesson's bus model?
Pause at a bus stop, which represents a safepoint where the engine can understand the thread state.
A search box sometimes janks on a real website. What should you measure before blaming garbage collection?
Record a performance trace first. Then decide whether the problem is app work, rendering, allocation pressure, or something else.
Check your understanding
For each question, identify whether it is about a color, a slice, a helper, or a planned coordination point.
Question 1 of 7In tri-color marking, what does grey mean?
Choose an answer to see the explanation.
Question 2 of 7What does this slice model print?
Read the code, then predictconst work = ["user", "cart", "items", "price"]; const size = 2; console.log(Math.ceil(work.length / size));Choose an answer to see the explanation.
Question 3 of 7Why does a marking barrier turn a new white child grey?
Choose an answer to see the explanation.
Question 4 of 7What is a bailout worklist for?
Choose an answer to see the explanation.
Question 5 of 7What does work stealing solve?
Choose an answer to see the explanation.
Question 6 of 7Why does a collector use a safepoint?
Choose an answer to see the explanation.
Question 7 of 7What does Orinoco name in V8?
Choose an answer to see the explanation.
Key takeaways
- White is unvisited, grey is discovered but unfinished, and black is fully checked in a marking cycle.
- Incremental marking splits work into pauses; smaller slices can reduce the longest pause.
- Concurrent helpers need barriers and may leave special objects on a bailout worklist.
- Parallel workers use worklists and stealing to divide collector work; safepoints coordinate short pauses safely.
- Idle-time work uses spare frame time, while Orinoco is V8's mostly parallel and concurrent collector project.
Remember the one-liner.
Modern garbage collectors preserve reachability while carefully scheduling the work that proves it.
Coming next: Weak references & ephemerons.