⚛ The Course  /  Module 9 · Practice & Debugging · 9.3
Module 9 · Practice & Debugging

9.3 Wrap-up and where to go next

You have now walked every layer of React's render engine, one mechanism at a time — Fibers, the two trees, Lanes, the Scheduler, and all the machinery hung on them. The danger at the end of a long course is that the pieces stay scattered in your head. This closing lesson puts them back into one picture: it recaps the whole course against the four pillars you met on day one, shows the single diagram that connects every piece, and points you to where to read next.

Two systems, two questions

If you remember one sentence from this course, make it this: the engine runs on two cooperating systems that answer completely different questions. The Scheduler answers when to work. Lanes answer what to work on.

System Question answered Mechanism Purpose
Scheduler (browser-level) WHEN to work Time-based: shouldYield() every ~5 ms Keep UI responsive — never block the main thread
Lanes (React-level) WHAT to work on Priority-based: getNextLanes() returns highest-priority bits Prioritize important updates; enable selective rendering

The Scheduler is a generic library that only knows time: work for about five milliseconds, then hand control back to the browser, then resume. Lanes are a React concept that only knows priority: a single 32-bit integer per fiber whose bits say which updates are urgent.

Two ways work stops

Because there are two systems, there are two completely different ways an in-progress render can stop. They are easy to confuse, so the course separated them carefully.

Type Controlled by What happens Work preserved?
Yielding Scheduler (shouldYield()) Pause and resume the same work Yes — resumes from where it stopped
True interruption Lanes (getNextLanes()) Abandon in-progress work and start fresh No — thrown away entirely

Yielding is a pause: the same work continues later. True interruption is a restart: the half-built draft is thrown away and a fresh render begins for a higher-priority lane. The draft can be discarded safely because of the two-trees pillar — the user is always looking at the other tree.

The four pillars, one more time

Everything else in the course is detail hung on four load-bearing ideas. Here they are with the one job each one does, and the module where you saw it proven with real source.

PillarIts one jobProven in
FibersThe work, shaped as a tree — one pausable unit of work that also stores state, effects, and a link to the DOM.Module 1
Two trees (double buffering)A hidden draft vs what's on screen, linked by .alternate and swapped atomically after commit.Module 1 (1.3)
LanesWhich work is urgent — a 32-bit integer where each bit is a priority.Module 3
The SchedulerWhen work runs — yield to the browser every ~5 ms so the page never freezes.Module 4

Those are the four pillars from lesson 0.2. The deeper chapters added two more orthogonal ideas that lean on the pillars: the render / commit split (render is in-memory and interruptible; commit is synchronous and atomic — Modules 2 and 5) and hooks as linked lists (state and effects live on the fiber itself, so they survive interruption and resumption — Module 6). Four pillars plus those two ideas are the whole engine.

Three ideas that tie the pillars together

A few mechanisms only make sense once you see two pillars cooperate. These came up again and again, so they are worth restating.

Double buffering enables safe abandonment. The current tree is never modified during render, so workInProgress can be thrown away at any time without affecting what the user sees. That is what makes true interruption cheap.

The commit is all-or-nothing. The render phase is in memory (pausable, interruptible). The commit phase is uninterruptible: there are no shouldYield() checks in it. The user never sees half an update.

Bailout takes all four conditions. Even when the lane check would let React skip a fiber, every other reason to render must also be absent:

Bailout = Props unchanged
        AND Context unchanged
        AND No lane update on this fiber
        AND No error flags

If ANY condition fails → component renders (even if the lane check passes)

And when a component does render, lanes still gate which state updates are applied inside it — selective updating within a single component, not just across components. (You saw that in Module 5; the proof is in the deep dive below.)

The whole engine in one diagram

This is the map. One user action enters at the top; a browser paint comes out at the bottom. Read it slowly — you have met every line on it by name.

User Action
    │
    ▼
Lane Assignment        ← what priority? (event type → getEventPriority → requestUpdateLane)
    │
    ▼
FiberRoot.pendingLanes ← track globally (markUpdateLaneFromFiberToRoot propagates upward)
    │
    ▼
ensureRootIsScheduled  ← called after every event AND after every render pass
    │
    ▼
getNextLanes           ← select highest-priority lanes only
    │
    ▼
scheduleCallback       ← create ONE Scheduler Task object
    │
    ▼
Scheduler executes when browser is ready
    │
    ▼
Render Phase (in memory — interruptible)
    ├─ shouldYield()? → Pause / Resume (Scheduler controls timing)
    └─ Higher-priority lane arrived? → Abandon / Restart (Lanes control priority)
    │
    ▼
Commit Phase (DOM update — synchronous, uninterruptible, all-or-nothing)
    ├─ commitMutationEffects() → DOM changes (appendChild / removeChild / property updates)
    ├─ root.current = finishedWork → atomic tree swap
    └─ commitLayoutEffects() → useLayoutEffect (blocks paint)
    │
    ▼
Browser Rendering Pipeline (automatic after JS returns to event loop)
    Style → Layout → Paint → Composite → Display
    │
    ▼
flushPassiveEffects    ← useEffect runs here, after paint

Notice where each pillar shows up. Lanes appear at the top (assigning priority) and inside the render phase (deciding when to abandon). The Scheduler appears between scheduling and render (deciding when to run and when to yield). The two trees appear at the commit (the atomic swap). And the render / commit split is the boundary in the middle.

One more timing detail that trips people up — where the two effect hooks run relative to that final paint:

useLayoutEffect:  Render → Commit → useLayoutEffect (synchronous, BLOCKS paint) → Paint
useEffect:        Render → Commit → Paint → useEffect (asynchronous, after paint)

Where to go next

You have reached the end of the lessons, but not the end of the material. Two next steps:

  • The Appendix & glossary collects every term in one place — Fiber, lane, beginWork, renderLanes, subtreeFlags, and the rest — so you can look up a word without re-reading a whole lesson.
  • The real React source. Every code block in this course was copied verbatim from it. Now that you know the vocabulary, the files in react-reconciler (where beginWork, completeWork, and the commit phase live) and scheduler (where shouldYieldToHost lives) read as prose. Open one and follow a single function — you will recognize the shape.

The real code

Two short proofs that pull the whole picture together. First, the commit ordering — DOM mutations happen before the tree swap, which is the render / commit split made concrete:

commitMutationEffects(finishedWork);   // Apply DOM changes FIRST
root.current = finishedWork;           // Switch to new tree AFTER

// Order matters: if an error occurs during DOM mutations,
// React's internal state is still consistent with the old tree.

Second, the Lanes pillar in three operations — one number storing many priorities, with O(1) bitwise math for add, check, and remove:

fiber.lanes |= newLane;               // Add a lane (bitwise OR)
(fiber.lanes & renderLanes) !== 0;    // Check membership (bitwise AND)
fiber.lanes &= ~lane;                 // Remove a lane (AND with complement)

// Example: fiber.lanes = 68 = 0b1000100
// Contains InputContinuousLane (4) AND TransitionLane1 (64) simultaneously
// Each bit position never conflicts — O(1) for all operations
Show more source — interruption, immutability, and hook-level lane filtering

The two kinds of stop, written out as a comment from the chapter:

// Yielding (Scheduler):
// Rendering 10,000 items → shouldYield() fires at item 100 → yield to browser
// → Resume from item 101 (same work continues)

// True Interruption (Lanes):
// Rendering a transition (lane 0b1000) at item 100
// → User types → SyncLane (0b0001) arrives
// → Throw away items 0-100, start a NEW render with renderLanes = 0b0001

Why immutability is a correctness requirement, not a style choice — a new reference is the only thing React's Object.is checks can see:

// Wrong — mutates, same reference, bailout fires
user.name = 'Bob';
setUser(user);

// Correct — new reference, React sees a real change
setUser({ ...user, name: 'Bob' });

And lane filtering at the hook level — a component can render while only some of its state updates are applied:

// Render Pass 1 — renderLanes = 0b0001 (SyncLane):
const [query,   setQuery]   = useState(''); // lane 0b0001 → Applied  ✅ (query = 'r')
const [results, setResults] = useState([]); // lane 0b1000 → Skipped  ❌ (results = [])

// Component renders with query='r' but results=[] (old value!)
// The results update is only applied in Render Pass 2 (renderLanes = 0b1000)