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
Top comments (0)