cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Write barriers & remembered sets

Learn how JavaScript engines record old-to-young pointers so fast minor collections can keep young objects alive without scanning old memory.

By the end, you can
  • 01
    Spot the missing pointerExplain why an old object pointing to a young object must be recorded before a minor collection.
  • 02
    Follow the collector's notesDistinguish remembered sets, store buffers, and card marking in small teaching models.
  • 03
    Reason about write costExplain why optimized code checks stores and when an engine can safely skip that check.

The pointer a minor GC could miss

A minor collection cleans the young generation. It is fast because it does not open and scan every old object. Most fresh objects die quickly, so looking closely at young memory is usually enough work for one short collection.

A long-lived cart receives a fresh itemPop out in the code editor (opens in a new tab)JavaScript
const cart = []; // long-lived, so imagine it is oldcart.push({ item: "tea" }); // fresh object, so imagine it is youngconsole.log(cart[0].item);

Line 1 creates cart. The comment gives it the old-generation role for this model. Line 2 creates a fresh tea object in the young generation. Line 3 stores that young object inside the old cart and prints tea.

That assignment creates the problem. A young-only collector can see young roots and young objects, but it deliberately skips the old cart. If no direct young root points at tea, the collector needs another way to discover that the old cart still needs it.

Definition

A write barrier is small collector bookkeeping attached to a memory write. When an old object receives a young object, the barrier records enough information for a later minor collection to find that young object.

Real-life analogyYour phone's Downloads folder and Photos album

Imagine cleaning your phone's Downloads folder without opening every Photos album. If an album entry links to one download, deleting that file would break the album. When you add the link, put a sticky note on Downloads saying that one album entry uses it.

In real life: Downloads is cleaned quickly
In JavaScript: Young generation gets a minor collection
In real life: Photos album stays unopened
In JavaScript: Old generation is not fully scanned
In real life: Album entry links to a download
In JavaScript: Old object points to a young object
In real life: A sticky note names that link
In JavaScript: Remembered information records the write

Where the analogy stops: A phone app has human-designed folders. A collector uses machine metadata and follows pointers, not file meaning.

The sticky-note habit is the barrier. The note itself becomes remembered information. This lesson models that information plainly; it does not inspect your browser's real heap or promise one engine's exact data structure.

Old-to-new pointers

An old-to-new pointer is a reference held by an old object whose value is young. The direction matters. A young object pointing to another young object is already inside the area a minor collection visits. An old object pointing to an old object does not reveal any young object.

Name the direction of the pointerPop out in the code editor (opens in a new tab)JavaScript
const cart = { generation: "old" };const tea = { generation: "young" };console.log(cart.generation + "-to-" + tea.generation);

Line 1 makes an old cart model object. Line 2 makes a young tea model object. Line 3 joins those labels and prints old-to-young. The labels are teaching data, not properties exposed by JavaScript engines.

Think about the order of events. The program writes a pointer first. The barrier runs beside that write and checks whether it crossed from old memory into young memory. If it did, the barrier leaves a note before a future minor collection needs it.

Step through one old-to-young write
Step 0 of 7Ready
Your turn: follow the blue line

Replay instrumented teaching code. This is not a view into your browser's real remembered set.

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
const tea = { generation: "young" };const rememberedSet = []; function writeField(owner, key, value) {  owner[key] = value;  if (owner.generation === "old" && value.generation === "young") {    rememberedSet.push({ owner, key, value });  }} writeField(cart, "latest", tea);console.log(rememberedSet.length);
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 calls the real writeField model. Notice that the old cart is still not a minor root. The remembered slot is the bridge from a collection that mostly sees young memory to a value stored behind old memory.

Remembered sets

A remembered set is collector metadata about pointers a young collection must revisit. In this lesson's smallest model, every entry names an owner, one field key, and the young value that field currently holds.

Record one relevant fieldPop out in the code editor (opens in a new tab)JavaScript
const rememberedSet = []; function writeField(owner, key, value) {  owner[key] = value;  if (owner.generation === "old" && value.generation === "young") {    rememberedSet.push({ owner, key, value });  }} const cart = { generation: "old" };const tea = { generation: "young" };writeField(cart, "latest", tea);console.log(rememberedSet.length);

Line 1 starts the remembered set empty. Line 3 stores the field. Lines 5 and 6 test for an old owner and young value, then append a slot record. Line 12 writes tea into the old cart, and line 13 prints 1 because one remembered slot was added.

Real engines avoid storing JavaScript objects exactly this way. They may record addresses, object regions, or other compact metadata. The important contract stays the same: a later minor collection can find every relevant old-to-young edge without scanning all old space.

Four kinds of information a minor collection may use
ThingWhat it describesWhy it matters
Young rootA direct root that points into young space.Minor collection scans it every time.
Remembered slotAn old-to-young pointer recorded by a barrier.Minor collection treats its young value like an extra root.
Store bufferA short list of recent writes waiting to be processed.The engine later turns entries into remembered information.
Dirty cardA coarse marker for a small old-memory region.The collector rescans that region, not every old slot.

Minor collection uses the notes

The remembered set is useful only when collection reads it. A minor collector starts with direct young roots, then treats young values found through remembered slots as extra roots. From there it follows young-to-young references in the usual way.

A remembered slot keeps tea livePop out in the code editor (opens in a new tab)JavaScript
const youngRoots = [];const rememberedSlots = [{ owner: "cart", key: "latest", value: "tea" }]; function minorCollection(roots, slots) {  const live = new Set(roots);  for (const slot of slots) live.add(slot.value);  return [...live].sort();} console.log(minorCollection(youngRoots, rememberedSlots).join(","));

Line 1 says there are no direct young roots. Line 2 still supplies a remembered slot from cart to tea. Line 5 makes a live set, and line 6 adds each noted value. The final line prints tea, showing that the note alone kept tea in this model.

This is the central speed trade. The collector avoids every old object, but it must keep the remembered information correct. Missing one note would be a correctness bug: a live young object could look dead merely because its only path started in old memory.

Step through a minor collection with a note
Step 0 of 5Ready
Your turn: follow the blue line

Replay a minor-collection model. It treats remembered slots as extra young roots; it does not collect your browser heap.

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
const tea = { generation: "young" };const rememberedSet = [{ owner: cart, key: "latest", value: tea }]; function minorCollection(youngRoots, slots) {  const live = new Set(youngRoots);  for (const slot of slots) live.add(slot.value);  return [...live];} console.log(minorCollection([], rememberedSet).map((value) => value === tea ? "tea" : "?").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.

Remembered sets and store buffers

A store buffer is a small list of recent relevant writes. It can separate the immediate job of noticing a write from the later job of organizing that information into a remembered set. The names vary between engines, but the idea is common.

Buffer a relevant writePop out in the code editor (opens in a new tab)JavaScript
const storeBuffer = []; function store(owner, key, value) {  owner[key] = value;  if (owner.generation === "old" && value.generation === "young") {    storeBuffer.push({ owner, key, value });  }} const cart = { generation: "old" };const tea = { generation: "young" };store(cart, "latest", tea);console.log(storeBuffer.length);

Line 1 creates an empty buffer. Lines 3 through 6 store a field and push a record only for an old-to-young write. Line 12 performs that write, so line 13 prints 1. A later engine phase can process this little list.

Buffering does not allow the engine to forget the write. It changes when and where the bookkeeping is finished. The collector must see all relevant records before it decides which young objects are live.

Keep the names separate

A store buffer is often the short-term queue of writes. A remembered set is the information minor collection consults. An engine can use different representations, but both exist to protect fast young-only scanning.

Card marking

Recording every exact slot can be more detail than an engine wants on every write. Card marking divides old memory into small regions called cards. A relevant write marks its card dirty, and a minor collection later checks all slots in that marked region.

Mark the card containing slot 5Pop out in the code editor (opens in a new tab)JavaScript
const cardSize = 4;const cardTable = [false, false, false];const slot = 5;const card = Math.floor(slot / cardSize);cardTable[card] = true;console.log(cardTable.join(","));

Line 1 gives each card four slots. Line 3 chooses slot 5. Line 4 divides by four and gets card 1. Line 5 marks that card, and line 6 prints false,true,false. Only the middle region is dirty.

A dirty card is deliberately less precise than one remembered slot. It may contain several fields, and some may no longer point to young objects when collection begins. That extra rescan work is the price of much smaller, cheaper write-time metadata.

Playground: mark one four-slot card
Four-slot card modelPop out in the code editor (opens in a new tab)JavaScript
const cardSize = 4;const cardTable = [false, false, false];const slot = 5;const card = Math.floor(slot / cardSize);cardTable[card] = true;console.log(cardTable.join(","));
Card table0 dirty
card 0clean

Slots 0 through 3

card 1clean

Slots 4 through 7

card 2clean

Slots 8 through 11

Try it yourself

Slot 5 belongs to card 1. The dirty cards are none. A minor collector would rescan only those cards in this model.

This is a teaching model. Real engines choose card sizes and metadata layouts that fit their own heap design.

The cost of barriers in JIT code

A property write in optimized code is not always one simple machine action. The JIT compiler may add a few checks so it can tell whether the write could create an old-to-young pointer. This is a normal cost of a moving, generational collector.

A simple barrier checklistPop out in the code editor (opens in a new tab)JavaScript
function store(owner, value) {  if (valueIsSmallInteger(value)) return "no pointer";  if (owner.generation === "young") return "young owner";  if (value.generation === "old") return "old value";  recordOldToYoung(owner, value);  return "recorded";} console.log("recorded");

Line 2 skips a small integer because it is not a heap pointer. Line 3 skips when the owner is young because minor collection will scan it. Line 4 skips an old value because the write does not point into young memory. Only line 5 records the remaining possible case.

This is an illustration, not assembly and not a contract for one engine. Engines use type feedback, object layouts, inline caches, and slow paths to make common safe cases cheap. When proof is unavailable, correctness wins and the barrier stays.

See three checklist outcomesPop out in the code editor (opens in a new tab)JavaScript
function barrierDecision(ownerGeneration, value) {  if (typeof value === "number") return "skip: primitive";  if (ownerGeneration === "young") return "skip: young owner";  if (value.generation === "old") return "skip: old value";  return "record: old-to-young";} console.log(barrierDecision("old", 3));console.log(barrierDecision("young", { generation: "young" }));console.log(barrierDecision("old", { generation: "young" }));

Line 2 recognizes the primitive number and skips pointer work. Line 3 skips a young owner because minor collection will scan it anyway. Line 5 reaches the recording path only for an old owner with a young object value. The three logs therefore print skip: primitive, skip: young owner, and record: old-to-young.

The important performance idea is not that every assignment is expensive. It is that a JIT must preserve this collector invariant while keeping common, provably safe stores short. A few well-predicted checks are often much cheaper than scanning all old memory later.

What application code should know

Application code does not call a public writeBarrier() function after each assignment. JavaScript engines insert their own bookkeeping under ordinary property writes, array writes, and other pointer stores. Your source code remains ordinary JavaScript.

Ordinary code still makes ordinary assignmentsPop out in the code editor (opens in a new tab)JavaScript
const cart = [];cart.push({ item: "tea" });console.log(cart.length);

Line 1 creates an array. Line 2 stores a new object in it. Line 3 prints 1. If this array becomes old and the item is young, the engine may run a barrier internally. The program does not need to know which remembered representation the engine chose.

Use this knowledge to read profiles carefully. Many small writes can have collector cost, but changing code based on a guess is rarely wise. Measure a real slowdown first, then use the engine and browser tools appropriate to that app.

Would the barrier record it?
  • An old cart stores a young tea object.
  • An old cart stores the number 3.
  • A young cart stores a young tea object.
  • An old cart stores an old user object.
  • Slot 9 changes in an old page with four slots per card.
  • An old list stores the primitive price 120.
Try it yourself
0 of 6 correct

Sort each situation by the lesson's simplified decision. Read the explanation after placing every card.

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

Common misconceptions

These names are easy to blur because they all sit below ordinary JavaScript. Keep each job narrow: the barrier notices a write, remembered information exposes an old-to-young edge, and card marking chooses a small region to rescan.

  • “I should call a barrier from my app.” No. It is engine machinery behind normal writes.
  • “A remembered set copies all old objects.” No. It records only information needed for young collection.
  • “A dirty card names one exact pointer.” No. It names a region that must be rechecked.
  • “Minor collection can ignore old memory completely.” No. It avoids a full scan but must use remembered information.
  • “Every write has the same cost.” No. JIT code can prove many stores need no pointer barrier work.
Terms that sound similar but do different jobs
TermWhat it isDo not confuse it with
Write barrierA small action on a write that may record a pointer.A JavaScript API you call yourself.
Remembered setInformation about old-to-young references.A complete copy of all old objects.
Card markingMarking a region as possibly containing a useful pointer.Knowing the exact slot without rechecking.
Minor collectionA young-generation collection that avoids scanning all old memory.A collection that can ignore old-to-young pointers.

The lesson models one common pattern, not a specification for every JavaScript engine. V8, JavaScriptCore, SpiderMonkey, and embedded engines can choose different details while preserving the same language behavior.

Practice exercises

Exercise 1 · Warm-upName the pointer

An old cart stores a fresh tea object. What kind of pointer should the lesson model record?

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

    Exercise 2 · Warm-upPredict the generation check

    Read the small generation model and type the exact output.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const cart = { generation: "old" };
    const tea = { generation: "young" };
    console.log(cart.generation === "old" && tea.generation === "young");

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

      Exercise 3 · PracticeKeep tea alive

      Tea is stored only through an old cart. What information lets the model minor collection retain tea?

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

        Exercise 4 · PracticeFind the card

        In a four-slot card model, which card contains slot 9?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const cardSize = 4;
        const slot = 9;
        console.log(Math.floor(slot / cardSize));

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

          Exercise 5 · ChallengeSkip a safe write

          Explain why an old object storing the number 3 can skip the pointer-barrier recording step.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const owner = { generation: "young" };
          const tea = { generation: "young" };
          console.log(owner.generation === "young" ? "skip" : "record");

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

            Exercise 6 · ChallengeApply it to a real website

            A product grid feels slow after a change with many updates. What should you do before rewriting its assignments around a guess about barriers?

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

              Check your understanding

              Use the direction of a pointer first, then decide whether the collector needs an exact slot, a buffered record, or a dirty card.

              Write barriers and remembered sets quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Why does a minor collection need remembered information?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const cart = { generation: "old" };
                const tea = { generation: "young" };
                console.log(cart.generation === "old" && tea.generation === "young");

                Choose an answer to see the explanation.

              3. Question 3 of 7What is a remembered set in this lesson's model?

                Choose an answer to see the explanation.

              4. Question 4 of 7A store buffer is best described as what?

                Choose an answer to see the explanation.

              5. Question 5 of 7What does card marking trade for less immediate metadata?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which store can an optimized engine often skip for this barrier?

                Choose an answer to see the explanation.

              7. Question 7 of 7Does application code normally call a write barrier directly?

                Choose an answer to see the explanation.

              Key takeaways

              • Minor collection stays fast by scanning young memory instead of all old memory.
              • An old-to-young pointer must be recorded so a live young object is not missed.
              • Remembered sets, store buffers, and card tables are different ways to preserve or organize that information.
              • Card marking trades exact slot detail for rescanning a small dirty region later.
              • JIT code adds small write checks but skips them when it can prove a store is safe.
              • Application code normally relies on the engine and profiles real problems instead of calling barriers directly.

              Remember the one-liner.
              A write barrier leaves a note when old memory starts pointing at young memory.

              Coming next: Incremental, concurrent & parallel GC, where collectors do more bookkeeping to avoid one long pause while JavaScript keeps making progress.

              CompleteFrontend Clear concepts. Working examples.