In Part 1, we opened up React's Fiber engine and looked at the basics: what a Fiber node is, how the linked list tree is wired with child/sibling/return pointers, how the two-phase walk (beginWork down, completeWork up) processes every component, and how reconciliation diffs old Fibers against new elements.
Now we go deeper. This part covers the systems that make Fiber actually powerful: double buffering, effect flags, priority lanes, where your hooks live, and the three sub-phases of the commit. By the end, you'll understand exactly why React can pause rendering mid-tree and come back later without breaking anything.
Double Buffering: Current vs. Work-In-Progress
Here's a clever trick React borrows from game rendering: double buffering.
At any given moment, React maintains two Fiber trees:
-
The
currenttree is what's actually on screen right now -
The
workInProgresstree is the next version React is building
Each Fiber in one tree has an alternate pointer to its counterpart in the other tree. When React starts a render, it walks the workInProgress tree, updating Fibers based on new props and state. The current tree stays untouched, so your UI remains stable while React works.
Once the render finishes and React commits the changes to the DOM, the trees swap. The workInProgress becomes the new current, and the old current becomes the next workInProgress (ready to be recycled for the next render).
Before commit:
current tree ←── on screen
workInProgress tree ←── being built
After commit:
old workInProgress ←── now it's "current" (on screen)
old current ←── now it's the next "workInProgress"
This is why React can build your next UI without any flickering or tearing. The screen only updates once, at commit time, when everything is ready.
Effect Flags: Telling the Commit Phase What to Do
During the render walk, React doesn't touch the DOM. Instead, it marks Fibers with effect flags, tiny bitmask annotations that say "this node needs attention."
There are three main ones:
- Placement means this is a new node that needs to be inserted into the DOM
- Update means this node's props changed and the DOM needs updating
- Deletion means this node was removed and needs to be cleaned up
const NoFlags = 0b0000
const Placement = 0b0010 // New node → insert into DOM
const Update = 0b0100 // Props changed → update DOM
const Deletion = 0b1000 // Removed → delete from DOM
React also aggregates flags upward through subtreeFlags. If none of your children have any flags, React can skip the entire subtree during commit. That's a major optimization.
The actual DOM mutations happen in the commit phase, which runs synchronously and can't be interrupted. React reads the flags and performs insertions, updates, and deletions in a single pass.
Priority Lanes: Not All Updates Are Equal
Not every state update needs to happen right away. Typing in a search input is urgent. Updating a filtered list based on that input is less so. Fiber handles this through lanes, a bitfield-based priority system.
Each Fiber has a lanes field that tells React how urgent its pending work is. When you call setState, React assigns a lane to that update based on the context:
- SyncLane is for urgent work (discrete events like clicks, input changes)
-
TransitionLane is for
useTransitionandstartTransitionupdates - IdleLane is for background work that can wait
const SyncLane = 0b0000001 // Click, input: do it now
const TransitionLane = 0b0001000 // startTransition: can be interrupted
const IdleLane = 0b1000000 // Background: whenever you have time
During the work loop, React checks which lanes have pending work and processes the highest-priority ones first. If a TransitionLane render is in progress and a SyncLane update comes in, React can interrupt the current work, handle the urgent update, then come back to the transition.
The childLanes field on each Fiber tells React whether any descendants have pending work. If childLanes is empty, React can skip that entire subtree without even looking inside.
Where Hooks Live on the Fiber
You might wonder where your useState and useEffect calls actually store their data. The answer is memoizedState on the Fiber node. But it's not a single value. It's a linked list of hook objects.
Each time you call a hook in your component, React creates (or reuses) a hook object and appends it to the list:
// What a hook node looks like (simplified)
interface Hook {
memoizedState: unknown // The stored value (state, ref, memo result)
queue: UpdateQueue // Pending state updates
next: Hook | null // Pointer to the next hook
}
// For a component with three hooks:
// fiber.memoizedState → hook1 → hook2 → hook3 → null
// useState useEffect useMemo
This is why hooks must be called in the same order every render. React doesn't match hooks by name. It matches them by position in this linked list. If you put a hook inside an if statement, the list shifts and every hook after it reads the wrong state.
It also explains why React.memo is effective. When React sees that a memoized component received the same props, it skips beginWork for that Fiber entirely. The memoizedState linked list stays untouched. No hooks run, no re-render, no wasted work.
The Commit Phase: Three Sub-Phases
After the render walk finishes and every Fiber has been tagged with its effect flags, React enters the commit phase. This is where the actual DOM mutations happen. Unlike the render phase, the commit phase is synchronous. It cannot be interrupted.
The commit runs in three sub-phases:
Before mutation: React reads from the DOM before changing anything. This is where getSnapshotBeforeUpdate runs for class components. It captures values like scroll position that might change after the DOM update.
Mutation: React walks the Fiber tree and performs the actual DOM operations based on the effect flags. Insertions, updates, deletions, all happen here. Ref detachments also happen in this phase (old refs get set to null).
Layout: React runs effects that need to read from the freshly updated DOM. This is where useLayoutEffect callbacks fire and refs get attached to their new DOM nodes. The DOM is updated but the browser hasn't painted yet, so you can measure layout without visual flicker.
After the layout phase, React flushes useEffect callbacks asynchronously. These run after the browser has painted, which is why useEffect is the default for most side effects.
Render phase (interruptible)
└─ beginWork / completeWork walk
└─ Tag fibers with effect flags
Commit phase (synchronous, can't pause)
├─ Before mutation → read DOM snapshots
├─ Mutation → insert, update, delete DOM nodes
└─ Layout → useLayoutEffect, attach refs
After paint (async)
└─ useEffect callbacks fire
Why This Matters: Interruptible Rendering
Here's the payoff. The whole reason Fiber exists is this: React can pause the render walk at any Fiber node and come back later.
With the old "stack reconciler" (pre-React 16), rendering was a single recursive call. Once it started, it had to finish. A big tree meant a long blocking operation and your app would freeze.
Fiber changed that. Because each unit of work is a simple object with pointers to the next one, React can:
- Pause after processing any Fiber (check if there's higher-priority work)
- Resume from exactly where it left off (just follow the next pointer)
-
Abort the current tree entirely (throw away the
workInProgressand start fresh)
This is what powers concurrent features. useTransition marks state updates as low-priority so React can interrupt them for urgent updates like typing in an input. Suspense pauses a subtree until data arrives without blocking the rest of the UI. And React.memo lets React skip entire branches by bailing out of beginWork when props haven't changed.
Mental model: Think of the Fiber tree as a renovation checklist for a house. Each room (Fiber) has notes about what work needs doing (effect flags). The contractor (React) walks through room by room (beginWork), writing down what's needed. Then walks back out (completeWork), tallying up the total job. Finally, the crew (commit phase) does all the physical work at once. The key insight: the planning walk can be paused and resumed. The physical work cannot.
Test Your Knowledge (Part 2)
Q1: What is the purpose of the 'alternate' pointer on a Fiber?
- [ ] Points to a backup copy in case of errors
- [ ] References the corresponding Fiber in the other buffer (current ↔ workInProgress)
- [ ] Links to an alternative component to render on failure
- [ ] Points to the previous version of the component's props
Q2: Why must React hooks be called in the same order every render?
- [ ] TypeScript requires it for type inference
- [ ] React matches hooks by position in a linked list on the Fiber, not by name
- [ ] The JavaScript engine optimizes for consistent call order
- [ ] It's a convention, not a technical requirement
Q3: What happens during the mutation sub-phase of the commit?
- [ ] React calls your function components again
- [ ] React reads DOM snapshots for scroll positions
- [ ] React performs actual DOM insertions, updates, and deletions based on effect flags
- [ ] React fires useEffect callbacks
Reveal all answers
Q1: References the corresponding Fiber in the other buffer (current ↔ workInProgress). React uses double buffering. The current tree is what's on screen, the workInProgress tree is being built. Each Fiber's alternate pointer connects it to its counterpart. After commit, they swap roles.
Q2: React matches hooks by position in a linked list on the Fiber, not by name. Hooks are stored as a linked list in fiber.memoizedState. Each render, React walks this list in order. If you skip a hook conditionally, every hook after it reads the wrong node, leading to bugs.
Q3: React performs actual DOM insertions, updates, and deletions based on effect flags. The mutation sub-phase is where React reads the effect flags on each Fiber and performs the corresponding DOM operations. This is the only phase that actually changes what's on screen.
Key Takeaways (Full Series)
A Fiber is a mutable JavaScript object. React's internal sticky note for every piece of your UI. It persists across renders, which is how React tracks state, hooks, and effects.
The tree is a linked list built with child, sibling, and return pointers. React walks it depth-first: beginWork goes down (running your components), completeWork bubbles up (preparing DOM changes). The walk can be paused and resumed at any node, and that's the foundation of concurrent React.
Reconciliation happens during beginWork, where React diffs old Fibers against new elements to figure out what changed. Lanes control priority so urgent updates jump the queue. Hooks live as a linked list on memoizedState, which is why call order matters.
Double buffering keeps your UI stable: React builds the next version in a workInProgress tree while the current tree stays on screen. They swap at commit time. The commit phase runs in three sub-phases (before mutation, mutation, layout), and it's the only part that can't be interrupted.
That's Fiber. Not magic. Just a well-designed linked list, a two-phase walk, a priority system, and the ability to stop and start wherever it needs to.
If you missed Part 1, start here.
Skills: React · Fiber Architecture · Reconciliation · Concurrent Rendering · Virtual DOM


Top comments (0)