A1 · Part 3 · Advanced
Reconciliation & Fiber
Render is a pure, interruptible calculation. Commit is a synchronous mutation. Everything follows from that split.
11On this page
The two phases
| Render | Commit | |
|---|---|---|
| What happens | Your components run; React builds a tree of work | The DOM is mutated, refs attach, layout effects run |
| Interruptible | yes | no |
| Can run twice | yes — and does, in Strict Mode | never |
| Side effects allowed | none | this is where they belong |
This is why components must be pure. React may start rendering, throw the work away because something more urgent arrived, and start again from the top. A component that mutated something outside itself would have done it twice, or half.
What a fiber is
A fiber is a plain object representing one unit of work — one component
instance, one DOM element. It holds the type, the pending props, the state
hooks, the flags describing work it needs, and pointers to its child,
sibling and return (parent).
{ type: Button, stateNode, memoizedProps, memoizedState,
child, sibling, return, flags, subtreeFlags, alternate }
Those pointers are the important part. Before Fiber, React reconciled with recursion — and a recursive call stack cannot be paused. A linked list can: React keeps a pointer to the next fiber, so it can stop after any node, hand control back to the browser, and resume from that pointer later.
The whole point of the rewrite
Fiber turned rendering from a recursive function you cannot interrupt into a loop over a data structure you can. Everything React shipped afterwards — time slicing, transitions, Suspense, streaming — needed that one capability.
Double buffering
There are two trees. current is what is on screen;
workInProgress is what is being built. Each fiber points at its
counterpart through alternate, and React reuses those objects
rather than allocating a new tree every render.
When the render finishes, React swaps the pointer — the work-in-progress tree becomes current in one assignment. If the render is abandoned instead, the in-progress tree is simply discarded and the screen never showed a partial state. This is why an interrupted render is invisible rather than glitchy.
The diffing rules, exactly
A true tree diff is O(n³). React gets to O(n) by making two assumptions that hold in practice:
- Different types produce different trees. If the element type at a position changed, React does not try to match children — it unmounts the whole subtree and mounts a new one. State, refs and DOM nodes below are all discarded.
- Keys identify children across renders. Within a list, a key tells React which new element corresponds to which old one, so it can move a node instead of rebuilding it.
<div><Counter /></div> → <span><Counter /></span>
// div became span → Counter unmounts and remounts, losing its state
This is the mechanism behind the two bugs from the beginner tier: a component defined inside another component is a new function — a new type — every render, so rule one destroys it each time; and index keys make rule two match the wrong items.
Lanes: not everything is equally urgent
Each update is tagged with a lane, a priority. A click is more urgent than a transition, which is more urgent than an offscreen prerender. React works through the highest-priority lane first, and can abandon lower-priority work when something urgent arrives.
setInput(value); // urgent — user is typing
startTransition(() => setResults(list)); // can be interrupted
Concurrent features are the public API over this. Without lanes, every update is equally urgent, and one expensive re-render blocks the keystroke behind it.
Walking the tree, concretely
beginWork(fiber) // going down: run the component, create child fibers
completeWork(fiber) // coming back up: finish the node, bubble its flags to the parent
React descends through child pointers calling
beginWork, and when a fiber has no child it calls
completeWork and follows sibling, or
return if there is no sibling. Depth-first, with an explicit
pointer instead of the call stack — which is precisely what makes it
pausable.
Between units, React checks whether it has run out of its time slice. If it has, it yields to the browser and schedules a continuation. The browser gets to paint, handle input, and run its own work; React picks up from the fiber it stopped at.
Bailouts: how React skips work
Not every fiber in the path gets re-rendered. React bails out when it can, and knowing the conditions explains most "why did this render" questions:
- Props are referentially equal and there is no pending state — React reuses the existing fiber and does not call your component.
- The element object is identical — passing
childrenstraight through means the same element, so the subtree is skipped even though the parent re-rendered. This is why the children pattern beatsmemo. - State was set to the same value — React may re-render once and then bail before touching the DOM.
A bailout stops the walk at that node, so an entire subtree is skipped in one check. That is why re-rendering is usually cheap and why the fix for a slow tree is often structural rather than a cache.
Flags, and how commit finds the work
During render React records what each fiber needs as flags
— placement, update, ref, snapshot, has-layout-effect — and deletions
are noted on the parent. As completeWork climbs back up, it
bubbles those flags upward: each fiber's subtreeFlags is the
combination of everything beneath it. So the root knows, without looking, whether
any work exists anywhere in the tree, and each parent knows whether it is worth
descending into a child.
Commit then walks the finished tree in three passes — before
mutation (snapshots), mutation (DOM changes, refs detached),
layout (refs attached, useLayoutEffect) — and in each
pass skips any subtree whose subtreeFlags show nothing to do. A
change in one leaf costs a walk down one path, not a scan of every component.
Older write-ups, and React 16 and 17, describe an effect list: a
linked chain of only the fibers with effects, threaded together during render.
React 18 replaced it with flags and subtreeFlags, so
if you read "commit walks the effect list", you are reading about an
implementation that no longer exists. The three passes and their guarantees are
unchanged.
That three-pass structure is why effect
timing is what it is: refs are attached in the layout pass, which is why
every effect can rely on ref.current, and why nothing can during
render.
Watch it happen: render, then commit
Work order so far
Only ItemB has changed. Step through: the render phase walks
down and back up, bubbling that one flag toward the root; then commit walks
the finished tree and skips every clean subtree.
Where the time actually goes
A common misreading is that the virtual DOM is fast. It is not — it is overhead that buys a programming model. Direct DOM manipulation is faster than diffing; what React gives you is not having to write it.
In a slow React app the cost is almost never the diff. It is your component functions running: an expensive calculation with no memo, a context provider high in the tree handing out a new object every render, a list rendering two thousand rows that should be virtualised. The Profiler tells you which.
Reading a Profiler flame chart
- Width is time spent in that component and its children.
- Grey means it did not re-render — memo or bailout worked.
- Turn on "record why each component rendered". "Parent rendered" on a component whose props never change points at a missing
memo; "context changed" points at an unmemoised provider value.
Interview answer, one sentence
"Fiber replaced recursive reconciliation with a linked-list walk, so render became interruptible while commit stayed synchronous — which is why components must be pure and why concurrent features are possible at all. Diffing is O(n) because of two assumptions: different types mean a new subtree, and keys identify children across renders."
Opens in the editor: write it, run it, and check it against real tests.
All 54 for this level →Finished reading?
0 of 57 chapters read in this series