This content originally appeared on DEV Community and was authored by Mohamad Msalme
Know WHY — Let AI Handle the HOW
In Part 1, we learned how React prioritizes updates using useDeferredValue
. But here’s the question that should be bugging you: How does React actually pause rendering mid-way and resume later?
What if I told you it’s not magic – it’s actually a clever data structure called Fiber that transforms your component tree into something React can pause, resume, and even throw away? Understanding this changes how you think about React performance forever.
The Problem React Had to Solve
Before we get to Fibers, let’s understand the problem:
function App() {
return (
<div>
<Header />
<MainContent>
<Article />
<Sidebar>
<Widget1 />
<Widget2 />
<Widget3 />
</Sidebar>
</MainContent>
<Footer />
</div>
);
}
Old React (before Fiber):
- Renders from top to bottom recursively
- Once started, MUST finish the entire tree
- No way to pause halfway through
- If user clicks during render, too bad – they wait
The Challenge: How do you make rendering interruptible without breaking everything?
Think Like a Book Reader for a Moment
Imagine you’re reading a book:
Without Bookmarks (Old React):
- Start chapter 1
- Must read straight through to the end
- Can’t stop mid-sentence
- Can’t remember where you were if interrupted
With Bookmarks (Fiber Architecture):
- Start chapter 1
- Can place a bookmark on any page
- Stop reading when something urgent comes up
- Resume exactly where you left off
- Can even decide “this chapter isn’t relevant anymore” and skip it
That’s exactly what Fiber does for React rendering.
What Is a Fiber? The Unit of Work
A Fiber is a JavaScript object that represents one unit of work in React. Each React element becomes a Fiber node.
// Your JSX
<DisplayCounter count={5} />
// Becomes a Fiber object (simplified):
{
// Identity
type: DisplayCounter, // Component function/class
key: null, // React key
// Tree structure (linked list!)
return: parentFiber, // Parent (going up)
child: firstChildFiber, // First child (going down)
sibling: nextSiblingFiber, // Next sibling (going across)
// Props and State
pendingProps: { count: 6 }, // New props to apply
memoizedProps: { count: 5 }, // Current props
memoizedState: null, // Current state
// THE KEY TO CONCURRENT MODE!
lanes: 0b0001, // Priority (binary flag)
childLanes: 0b0011, // Children's priorities
// Effects (what needs to happen)
flags: Update, // Bitwise flags
// Double buffering
alternate: otherVersionOfThisFiber
}
Why Linked List Instead of Array?
Array (Old React):
const tree = [
{ type: 'div', children: [
{ type: Header },
{ type: MainContent, children: [...] }
]}
];
// Problem: Can't easily pause mid-traversal
// Would need complex index tracking
Linked List (Fiber):
const fiber = {
type: 'div',
child: headerFiber, // ← Can pause here
};
const headerFiber = {
type: Header,
return: fiber, // ← Know where we came from
sibling: mainFiber, // ← Know where to go next
};
// Can pause at any fiber and resume later!
The Brilliant Part: Double Buffering
React maintains TWO complete fiber trees at all times:
- Current Tree – What’s displayed on screen right now
- Work-in-Progress Tree – What React is building
// Current tree (what user sees)
const currentTree = {
type: App,
child: {
type: 'div',
child: {
type: DisplayCounter,
props: { count: 5 },
alternate: wipFiber // ← Points to work-in-progress version
}
}
};
// Work-in-progress tree (what React is building)
const workInProgressTree = {
type: App,
child: {
type: 'div',
child: {
type: DisplayCounter,
props: { count: 6 }, // ← New count!
alternate: currentFiber // ← Points back to current
}
}
};
Why This Is Genius:
// React can work on work-in-progress tree
// User still sees current tree (stable UI)
// If interrupted by urgent update:
// → Just throw away work-in-progress tree
// → Start fresh with new priorities
// → No harm done!
// When rendering completes:
// → Swap pointers in ONE atomic operation
// → current = workInProgress
// → workInProgress = current
Real Example: Updating a Counter
function App() {
const [count, setCount] = useState(5);
const deferredCount = useDeferredValue(count);
return (
<div>
<DisplayCounter count={count} /> {/* Fiber A */}
<ExpensiveList count={deferredCount} /> {/* Fiber B */}
</div>
);
}
After user clicks (count becomes 6):
// CURRENT TREE (still visible on screen)
Fiber A: {
type: DisplayCounter,
props: { count: 5 }, // Old value
lanes: 0b0000,
}
Fiber B: {
type: ExpensiveList,
props: { count: 5 }, // Old value
lanes: 0b0000,
}
// WORK-IN-PROGRESS TREE (being built)
Fiber A (WIP): {
type: DisplayCounter,
props: { count: 6 }, // New value!
lanes: 0b0001, // SyncLane - HIGH PRIORITY
alternate: currentFiberA // Points to current tree
}
Fiber B (WIP): {
type: ExpensiveList,
props: { count: 5 }, // Still old (deferred!)
lanes: 0b1000, // TransitionLane - LOW PRIORITY
alternate: currentFiberB
}
Priority Lanes: How React Tracks Urgency
React uses binary flags for lightning-fast priority checks.
// Priority lanes (actual React source - simplified)
const SyncLane = 0b0000000000000000000000000000001; // Highest
const InputContinuousLane = 0b0000000000000000000000000000100;
const DefaultLane = 0b0000000000000000000000000010000;
const TransitionLane = 0b0000000000000000000001000000000; // Low priority
const IdleLane = 0b0100000000000000000000000000000; // Lowest
// Why binary? Super fast operations:
const hasUrgentWork = (lanes & SyncLane) !== 0; // Single CPU instruction!
const combinedWork = lanes1 | lanes2; // Merge priorities instantly
How useDeferredValue Sets Lanes
function App() {
const [count, setCount] = useState(0);
const deferredCount = useDeferredValue(count);
// When count updates:
// 1. Components using `count` get SyncLane (0b0001)
// 2. Components using `deferredCount` get TransitionLane (0b1000)
}
Internal priority assignment:
// Simplified React internals
function scheduleUpdate(fiber, newValue, isDeferred) {
if (isDeferred) {
// LOW PRIORITY - can be interrupted
fiber.lanes = TransitionLane;
fiber.pendingProps = newValue;
} else {
// HIGH PRIORITY - process immediately
fiber.lanes = SyncLane;
fiber.pendingProps = newValue;
}
// Bubble priority up to root
let parent = fiber.return;
while (parent) {
parent.childLanes |= fiber.lanes; // Bitwise OR combines lanes
parent = parent.return;
}
}
The Two Phases of Rendering
React’s work is split into two distinct phases:
Phase 1: Render Phase (Interruptible
)
This is where React can pause.
function workLoopConcurrent() {
// THE KEY: This loop can be interrupted!
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
// If shouldYield() returns true, we pause here
// workInProgress remembers where we stopped
// We can resume later from this exact fiber!
}
function performUnitOfWork(fiber) {
// Step 1: Call the component
const children = fiber.type(fiber.props);
// Step 2: Reconcile (diff) children
reconcileChildren(fiber, children);
// Step 3: Return next unit of work
if (fiber.child) return fiber.child; // Go down
if (fiber.sibling) return fiber.sibling; // Go across
return fiber.return; // Go up
}
Example: Rendering Pauses Mid-Tree
<App>
<Header /> {/* ✅ Rendered */}
<MainContent>
<Article /> {/* ✅ Rendered */}
<Sidebar>
<Widget1 /> {/* ✅ Rendered */}
<Widget2 /> {/* ⏸ PAUSE HERE - shouldYield() = true */}
<Widget3 /> {/* ⏳ Not rendered yet */}
</Sidebar>
</MainContent>
<Footer /> {/* ⏳ Not rendered yet */}
</App>
// React: "Used my 5ms time slice, better let browser handle user input"
// workInProgress = Widget2Fiber
// Can resume from here later!
Key Characteristics:
No DOM mutations yet
Can pause at any fiber
Can resume where left off
Can throw away all work if urgent update arrives
Pure computations only
Phase 2: Commit Phase (Uninterruptible
)
Once render phase completes, React commits changes atomically.
function commitRoot(root) {
const finishedWork = root.finishedWork;
// THIS PHASE CANNOT BE INTERRUPTED!
// Must complete to avoid inconsistent UI
// Sub-phase 1: Before mutation
commitBeforeMutationEffects(finishedWork);
// → Calls getSnapshotBeforeUpdate
// → Schedules useEffect
// Sub-phase 2: Mutation (THE CRITICAL MOMENT)
commitMutationEffects(finishedWork);
// → Actual DOM updates happen here
// → All at once, atomically
// Sub-phase 3: Switch trees (ATOMIC SWAP)
root.current = finishedWork;
// ↑ NOW the new tree is visible!
// Sub-phase 4: Layout effects
commitLayoutEffects(finishedWork);
// → Calls useLayoutEffect
// → Calls componentDidMount/Update
}
Why Must Commit Be Uninterruptible?
// Imagine if commit could pause mid-way:
function UserProfile() {
return (
<div>
<Avatar src={user.avatar} />
<Name>{user.name}</Name>
<Email>{user.email}</Email>
</div>
);
}
// If paused after Avatar but before Name/Email:
// DOM shows:
// Avatar for User B
// Name for User A ← INCONSISTENT!
// Email for User A ← INCONSISTENT!
//
// Users would see mixed data! 😱
Real-World Example: Search with Interruption
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => {
// Expensive filtering
return bigDataset.filter(item =>
item.name.includes(deferredQuery)
);
}, [deferredQuery]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<ResultsList items={results} />
</div>
);
}
What happens internally when you type “react”:
User types "r":
0ms: query = "r"
1ms: RENDER PHASE (HIGH PRIORITY)
→ Input fiber: lanes = SyncLane
→ Render <input value="r" /> ✅
2ms: COMMIT PHASE
→ Update DOM, input shows "r" ✅
3ms: RENDER PHASE (LOW PRIORITY)
→ ResultsList fiber: lanes = TransitionLane
→ Start filtering for "r"
→ ResultsList fiber #1 rendering...
→ ResultsList fiber #2 rendering...
10ms: User types "e" (NEW HIGH PRIORITY UPDATE!)
11ms: shouldYield() = true (time to check for urgent work)
12ms: React finds SyncLane work waiting
13ms: ABANDON work-in-progress tree (throw it away!)
14ms: RENDER PHASE (HIGH PRIORITY)
→ Input fiber: lanes = SyncLane
→ Render <input value="re" /> ✅
15ms: COMMIT PHASE
→ Update DOM, input shows "re" ✅
16ms: RENDER PHASE (LOW PRIORITY)
→ ResultsList: START NEW render for "re"
→ Old "r" filter abandoned, never committed!
The user never sees results for “r” – React intelligently skipped that intermediate state!
The Perfect Analogy: Construction Site with Blueprints
Think of React’s Fiber system like a construction site:
Current Tree = The actual building people are using
Work-in-Progress Tree = Blueprint and temporary scaffolding
- Workers can modify the blueprint/scaffolding all day
- People in the building don’t see any changes (stable!)
- If plans change, just throw away the scaffolding
- When blueprint is done, do the final construction (commit phase)
- Switch happens all at once – no half-renovated rooms
The Mental Model Shift
Stop Thinking:
- “React renders top to bottom”
- “Once rendering starts, it must finish”
- “State updates are immediate”
Start Thinking:
- “React renders fiber by fiber (unit by unit)”
- “React can pause between any two fibers”
- “React builds a new tree in the background”
- “Priorities determine which fibers render first”
- “Only commit phase makes changes visible”
The Takeaway
Many developers learn the HOW: “Use React hooks and it works.”
When you understand the WHY: “React uses Fiber nodes in a linked list structure with priority lanes, enabling interruptible rendering through double buffering,” you gain insights that help you:
- Understand why some updates feel instant and others don’t
- Know when to use useDeferredValue vs useTransition
- Debug performance issues by thinking about fiber priorities
- Make better architectural decisions
This content originally appeared on DEV Community and was authored by Mohamad Msalme