Things React Hides From Developers
Exploring the reconciliation engine, fiber architecture, and why your component re-renders seven times.
Problem
React is famously "magical." You update state, the UI updates. But what actually happens between setState and the DOM mutation is a black box for most developers.
Initial Assumptions
I thought React diffs virtual DOM trees recursively. This is what the docs implied with "the diffing algorithm."
The reality is more sophisticated. React uses a Fiber architecture — a linked list traversal that can be paused, resumed, and prioritized.
setState → enqueueUpdate → scheduleUpdate → workLoop
↓
performUnitOfWork
↓
beginWork → completeWork → next unit
↓
commitRoot → mutate DOM
React's scheduler runs the work loop when the main thread is idle. Each fiber node is a unit of work that can be interrupted.
Implementation
The Fiber Node
Each React element becomes a Fiber:
interface Fiber {
tag: WorkTag // FunctionComponent, ClassComponent, HostComponent, etc.
type: any // The actual component function/class
key: string | null
stateNode: any // Instance or DOM node
child: Fiber | null
sibling: Fiber | null
return: Fiber | null // Parent fiber
pendingProps: any
memoizedProps: any
memoizedState: any // Hook state stored here
effectTag: number // Placement, Update, Deletion, etc.
nextEffect: Fiber | null
alternate: Fiber | null // The "work-in-progress" tree
}
Reconciliation as a Linked List Walk
React doesn't recurse. It walks a linked list:
The fiber tree is a singly-linked list where each node has:
child— first childsibling— next siblingreturn— parent
Reconciliation is a depth-first traversal using a while loop, not recursion. This is what makes it interruptible.
function workLoop(deadline: IdleDeadline) {
while (nextUnitOfWork && deadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
if (nextUnitOfWork) {
requestIdleCallback(workLoop)
return
}
commitRoot()
}
Problems Encountered
Unnecessary Re-renders
Understanding the fiber tree explains why context consumers re-render even when the consumed value hasn't changed:
The issue is that React walks the fiber tree depth-first. When a context provider's fiber is marked as "needs update," all children are visited. The bailout happens at the child fiber level, but the traversal still happens.
function beginWork(fiber: Fiber): Fiber | null {
if (canBailout(fiber))
return cloneChild(fiber)
// ... process updates
return fiber.child
}
"Bailout" means React still visits the fiber to check if it can bailout. This is O(n) even for components that don't change.
Solutions ranked by effectiveness:
- Move state down — keep changing state close to where it's consumed
- useMemo / useCallback — stable references prevent child work
- React.memo — shallow comparison bailout at the fiber level
- Context splitting — don't put everything in one context
Each prevents work at a different level of the fiber walk.
Tradeoffs
| Strategy | Fiber visits saved | Mental overhead | Bundle impact |
|---|---|---|---|
| Move state down | Most | Low | None |
| useMemo | Some | Medium | Tiny |
| React.memo | Component subtree | Low | Tiny |
| Context splitting | Consumer subtree | Medium | Low |
| useCallback | Child render | Medium | Tiny |
| useMemo on context value | All consumers | Low | None |
Final Mental Model
The key insight: React's performance characteristics are determined by the fiber tree structure, not the component hierarchy.
Two components that look adjacent in JSX may be far apart in the fiber tree (because of fragments, forwardRef, memo wrappers). Conversely, deeply nested components that bailout are essentially free.
The mental model that helped me:
"Every fiber is a unit of interruptible work. The fiber tree is the execution plan, not the state snapshot. React builds a new plan every render cycle and executes it when the browser is idle."
Improvements
- React Compiler (automatic memoization)
- Better context selectors (like Zustand)
- Offscreen API for hidden trees
- Server components eliminate the fiber walk entirely for static subtrees
