Understanding React Reconciliation and Fiber Architecture
React's rendering engine is one of the most sophisticated pieces of the modern web stack. At its core lies reconciliation and the Fiber architecture — two concepts that fundamentally changed how React performs and handles complex UIs. This guide provides a deep technical understanding of how these systems work together.
The Problem: Synchronous Rendering
Before diving into Fiber, it's crucial to understand why it was needed. In React's early days (pre-16.0), the reconciliation process was entirely synchronous. Here's what that meant:
// Old Stack Reconciliation (Pre-Fiber)
function render(element) {
// If you call render, nothing else runs until it's done
// The main thread is blocked
// Browser can't handle user input, animations, or other tasks
}When React started rendering a tree, it couldn't stop mid-way. This caused jank — the UI would freeze when:
- Reconciling large component trees
- Re-rendering complex nested components
- Processing rapid state updates
The Synchronous Stack Problem
Main Thread Timeline (Synchronous)
[================== React Render ====================] [User Input] [Paint]
^ ^
Blocked for entire render Now you get input
Result: Janky UI, dropped frames, poor animation quality
Introducing React Fiber (v16.0+)
React Fiber fundamentally changed this by introducing incremental rendering — the ability to:
- Pause work and come back to it later
- Abort work if it's no longer needed
- Prioritize different types of work
- Reuse previously completed work
What is a Fiber?
A Fiber is a JavaScript object representing a component instance, DOM node, or hook state. It's the fundamental unit of work in React's reconciliation engine.
// Simplified Fiber structure
interface Fiber {
// Identifiers
type: string | Function; // Component type or tag name
key: string | null; // React key for list items
ref: any; // User-provided ref
// Component data
props: any; // Props passed to component
state: any; // Component state
memoizedState: any; // Cached state for hooks
// Tree structure
parent: Fiber | null; // Parent fiber
child: Fiber | null; // First child
sibling: Fiber | null; // Next sibling
// Effect tracking
effectTag: string; // 'Placement' | 'Update' | 'Deletion'
effects: Fiber[]; // Effects to run
// Work scheduling
expirationTime: number; // When this work expires
childExpirationTime: number; // When children work expires
// Reconciliation data
alternate: Fiber | null; // Previous version of this fiber
}The Fiber Tree Structure
React maintains two trees during reconciliation:
// Current Fiber Tree (what's rendered)
const currentRoot = {
type: 'div',
props: { className: 'app' },
child: {
type: Counter,
state: { count: 0 },
child: {
type: 'button',
props: { children: 'Count: 0' }
}
}
};
// Work-in-Progress Tree (being reconciled)
const wipRoot = {
type: 'div',
props: { className: 'app' },
child: {
type: Counter,
state: { count: 1 }, // Updated!
child: {
type: 'button',
props: { children: 'Count: 1' } // Updated!
}
},
alternate: currentRoot // Points back to current tree
};The Reconciliation Algorithm
React's reconciliation process uses a two-phase approach:
Phase 1: Render Phase (Can be Paused)
During this phase, React:
- Compares old and new fiber trees (diffing)
- Marks what needs to change
- Creates the work-in-progress tree
This is the interruptible phase where React can pause to let the browser handle user input or animations.
// Simplified reconciliation logic
function reconcile(fiber: Fiber, newProps: any): Fiber {
// Step 1: Compare props
if (!propsChanged(fiber.props, newProps)) {
return fiber; // No changes needed
}
// Step 2: Mark as needing update
const newFiber = {
...fiber,
props: newProps,
effectTag: 'Update',
alternate: fiber // Reference old fiber for comparison
};
// Step 3: Recursively reconcile children
reconcileChildren(newFiber, newProps.children);
return newFiber;
}
// Simplified reconciliation loop
let nextFiber = wipRoot;
let deadline = getTimeRemaining(); // Browser gives us time
while (nextFiber && deadline > 0) {
nextFiber = performUnitOfWork(nextFiber);
deadline = getTimeRemaining();
// If time's up, pause and let browser work
if (deadline <= 0) {
scheduleCallback(performWork); // Schedule continuation
break;
}
}Phase 2: Commit Phase (Not Interruptible)
Once all render phase work is done, React commits the changes to the DOM in one go:
// Simplified commit phase
function commitAllWork(fiber: Fiber) {
// Phase 2 is synchronous - once started, completes fully
// Step 1: Run all effects (setState, etc.)
commitEffects(fiber);
// Step 2: Attach to DOM
fiber.stateNode = createInstance(fiber);
// Step 3: Update DOM nodes
if (fiber.effectTag === 'Placement') {
commitPlacement(fiber);
} else if (fiber.effectTag === 'Update') {
commitUpdate(fiber);
} else if (fiber.effectTag === 'Deletion') {
commitDeletion(fiber);
}
// Step 4: Recursively commit children
commitAllWork(fiber.child);
commitAllWork(fiber.sibling);
}Work Prioritization and Scheduling
One of Fiber's superpowers is priority scheduling. Not all updates are equal:
// Priority levels in React
const ImmediatePriority = 99; // User interactions (clicks)
const UserBlockingPriority = 98; // Animations, form inputs
const NormalPriority = 97; // Data fetching, timers
const LowPriority = 96; // Non-essential updates
const IdlePriority = 95; // Lowest priority work
// React scheduler assigns priorities
function scheduleWork(fiber: Fiber, expirationTime: number) {
if (expirationTime === ImmediatePriority) {
// Do it NOW - user clicked
performWork();
} else if (expirationTime === UserBlockingPriority) {
// Schedule ASAP - animation frame coming
scheduleCallback(performWork);
} else {
// Can wait - low priority update
scheduleCallback(performWork, { delay: 5000 });
}
}
// Example: User interaction has higher priority than data fetch
function App() {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
const handleClick = () => {
// This update gets ImmediatePriority
setCount(count + 1);
// This update gets NormalPriority
fetchData().then(setData);
};
return <button onClick={handleClick}>Count: {count}</button>;
}The Diffing Algorithm (Reconciliation Rules)
React uses smart heuristics to compare trees efficiently:
Rule 1: Same Element Type = Update
// Old tree
<div className="a" />
// New tree
<div className="b" />
// Result: Update the existing div
// Reuse DOM node, update classNameRule 2: Different Element Type = Replace
// Old tree
<div />
// New tree
<span />
// Result: Destroy div fiber, create new span fiber
// Cannot reuse DOM nodeRule 3: Keys Help with Lists
// Without keys (inefficient)
function List({ items }) {
return (
<ul>
{items.map(item => (
<li>{item}</li> // Position-based matching
))}
</ul>
);
}
// Old: ['a', 'b', 'c']
// New: ['b', 'c', 'd']
// React thinks: 'a' → 'b', 'b' → 'c', 'c' → 'd'
// All 3 items need re-rendering!
// With keys (efficient)
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li> // Identity-based
))}
</ul>
);
}
// React knows: 'a' stays, 'b' moved, 'c' moved, 'd' added
// Only new item needs renderingPerformance: Before and After Fiber
Before Fiber (Synchronous)
Frame Time: 16ms (60fps target)
Scenario: Complex form with 50 input fields
[====== React Render (22ms) ======] <- Exceeds frame budget
[USER CAN'T INTERACT]
[Frame drops, animations stutter]
After Fiber (Incremental)
Frame Time: 16ms (60fps target)
Scenario: Same complex form
[== Render Work ==] [Browser Paint] [Animation] [== Continue ==]
5ms 5ms 2ms 4ms total
[USER CAN INTERACT]
[Smooth 60fps maintained]
Practical Code Example: Custom Hook with Fiber Concepts
Understanding Fiber helps you write better React code:
// useCallback memoization relies on Fiber's effect tracking
function memoizeCallback<T extends (...args: any[]) => any>(
callback: T,
deps: any[]
): T {
// React stores this in fiber's memoizedState
// Fiber knows when to invalidate the memoization
return useCallback(callback, deps);
}
// useMemo works similarly
function useExpensiveComputation(deps: any[]) {
return useMemo(() => {
// Fiber tracks if deps changed
// Skips recomputation if unchanged
return expensiveOperation();
}, deps);
}
// Understanding Fiber helps you batch updates
function useOptimizedState<T>(initial: T) {
const [state, setState] = useState(initial);
// Fiber scheduler will batch these automatically
const handleMultipleUpdates = () => {
setState(s => s + 1); // Batched
setState(s => s + 1); // Batched
setState(s => s + 1); // Batched
// All 3 updates execute in single render phase
};
return [state, handleMultipleUpdates];
}Key Takeaways
- Fiber is React's runtime - the JavaScript object representation of your component tree
- Reconciliation is incremental - React can pause and resume work to keep the UI responsive
- Two phases - Render phase (pausable) and Commit phase (atomic)
- Priority scheduling - User interactions get prioritized over background work
- Efficient diffing - Keys help React understand which items moved in lists
- Practical impact - Better performance with automatic batching and interruptible rendering
This understanding helps you write more performant React applications and debug rendering issues more effectively!
Further Reading
- React Fiber Architecture (React Team Blog)
- Lin Clark's "A Cartoon Intro to Fiber"
- React Reconciliation Documentation
- React Scheduler API for custom scheduling