In-Depth Article
React Diff Algorithm: How Reconciliation Uses Type, Key, and lastPlacedIndex
Understand how React reconciliation stays near O(n): single-node matching, array diffing in ReactChildFiber, keyed reuse, and move detection with lastPlacedIndex.
When people say "React's diff algorithm," the source usually calls it reconciliation. That is the process where React compares the current Fiber children with the next React elements and decides what can be reused, what should be deleted, and what needs to be inserted or moved.
The most important correction to make early is this:
React is not trying to compute the perfect minimum edit script for the whole tree.
If React used a general-purpose tree diff, the cost would be far too high for normal UI updates. Instead, React uses heuristics that keep the common path close to O(n).
This article focuses on the child reconciler in ReactChildFiber.js, because that is where the famous behaviors around type checks, keys, array diffing, and lastPlacedIndex actually live.
If you want nearby context first, this article pairs well with how JSX becomes DOM in React, how React updates move from setState to a DOM update, and how time slicing and Fiber traversal work.
1. The heuristic model: fast enough beats globally optimal
The classic reconciliation docs explain React's speed with two main assumptions:
- different element types usually produce different subtrees
- developers can mark stable children with
key
If you look at the actual child reconciler, a third practical rule becomes obvious:
- React only compares children within the same parent boundary, so cross-level reuse is not something it tries to optimize
That is why React can keep the algorithm fast:
- it does not search the whole tree for the globally best match
- it does not try to preserve a node that moved to an entirely different parent
- it focuses on sibling-level matching where UI updates usually happen
So the right mental model is not "React found the mathematically optimal edit sequence." It is:
React uses a set of local heuristics that are good enough for the vast majority of UI updates.
2. What the reconciler is actually comparing
During the render phase, the child reconciler compares two different shapes:
- old children: the current Fiber chain, linked by
childandsibling - new children: React elements, which may be a single element, an array, an iterator, text, or empty
That difference in shape is important.
The old side is a linked list. The new side is often an array of elements returned from JSX.
Because of that, React does not use one universal child-diff function for all cases. In ReactChildFiber.js, the work branches into specialized paths, especially:
reconcileSingleElement(...)reconcileChildrenArray(...)
Those two paths explain most of what developers mean by "the React diff algorithm."
3. Single-node diff: reconcileSingleElement
When the new child is a single React element, React takes the single-element path.
The matching logic is simpler than the array case, but it still follows a clear order:
- Scan the old sibling chain for a matching
key - If the key matches, check whether the type is still compatible
- If both key and type are compatible, reuse the old Fiber through
useFiber(...) - Otherwise, delete the old branch and create a new Fiber
That order matters.
Key comes first
Identity is checked before type reuse. If the key does not match, React keeps scanning siblings because the matching child might be later in the old chain.
Type decides whether reuse is allowed
Once a matching key is found, React checks whether the element type still matches:
- host type like
divvsdiv - component type like
UserCardvsUserCard - fragment/lazy compatibility in the special cases handled by the source
If the type is compatible, React clones the old Fiber into a new work-in-progress Fiber and updates the pending props.
If the type is different, React treats it as a remount.
Remaining siblings are deleted
A subtle but important point:
If the new child is only one element, then old siblings that are no longer needed get marked for deletion.
So single-element reconciliation is not just "reuse or recreate one node." It is also:
- keep the one compatible child
- delete everything else that used to sit beside it
4. Array diff: reconcileChildrenArray is the real core
The array path is where React's child diff becomes most interesting.
This is the case people usually mean when they talk about:
- reordering lists
- why
keymatters - why index keys can cause trouble
- how React detects moves
The source follows a two-phase strategy.
5. Phase one: scan the common prefix with updateSlot
React starts optimistically.
It walks the old Fiber list and the new children array at the same time, from left to right, trying to reuse items in the same relative position.
This is the fast path:
- compare the current old Fiber with the current new child
- let
updateSlot(...)decide whether they match - if they do, reuse and continue
- if they do not, break out and switch to the slower map-based path
This is fast because many real updates are simple:
- same order
- same keys
- maybe just different props
While that prefix match is working, React also calls placeChild(...) so it can keep track of whether reused nodes can stay where they are.
6. Fast-path exits: perfect match, pure insertions, or pure deletions
After the prefix scan, three easy cases can happen.
Case 1: new children are exhausted
If React has already consumed the whole new array, then any remaining old Fibers are deleted.
Case 2: old children are exhausted
If React has run out of old Fibers first, then every remaining new child is created as a new Fiber and marked for placement.
Case 3: both sides still have remaining items
This is the interesting case. It means the fast prefix assumption broke down somewhere in the middle.
That is when React switches to a Map-backed lookup.
7. Phase two: build a Map with mapRemainingChildren
Once the easy prefix is over, React creates a temporary Map from the remaining old children.
The keys for that Map are:
- the explicit
keyif a child has one - otherwise the old
index
This is what makes later lookups efficient.
Instead of scanning the rest of the old linked list again and again, React can look up a candidate old Fiber in approximately O(1) time while iterating through the remaining new array.
After that, React walks the remaining new children and calls updateFromMap(...).
For each new child:
- if a matching old Fiber is found, React reuses it
- if no match exists, React creates a new Fiber
- any old children left in the Map after the pass are deleted
So the second phase is basically:
restore as much reuse as possible, then delete whatever the new list no longer needs.
8. How moves are detected: placeChild and lastPlacedIndex
This is the most practical part of the algorithm.
Once React has found a child to reuse, it still has to answer:
Can this child stay where it is, or does it need to move?
That decision happens inside placeChild(...).
The key variable is lastPlacedIndex.
It records the largest old index React has seen so far among children that can stay in order.
The move rule is:
- if the reused child's old index is less than
lastPlacedIndex, mark it withPlacement - otherwise it can stay in place, and
lastPlacedIndexmoves forward to that old index
The simplified source logic looks like this:
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
return oldIndex;
}
} else {
newFiber.flags |= Placement;
return lastPlacedIndex;
}
That means Placement covers two different situations:
- a brand new insertion
- an existing child that must move
9. A concrete example: A B C D becomes A C B D
Suppose the old order is:
A -> B -> C -> D
And the new order is:
A -> C -> B -> D
Here is what happens conceptually:
Amatches early and stays in place.lastPlacedIndexbecomes0.Cis found later in the old list with old index2. Since2 >= 0,Ccan stay in place from React's perspective.lastPlacedIndexbecomes2.Bis found with old index1. Since1 < 2, React marksBwithPlacement.Dis found with old index3. Since3 >= 2,Dstays in place.
So React does not "move everything after the mismatch." It only marks the nodes whose relative order is now behind a node that already established a later stable position.
That is why lastPlacedIndex is such a good mental hook:
It turns list movement into a simple monotonic order check.
10. Why keys matter so much
Keys are not just there to silence a warning. They are how React preserves identity among siblings.
When keys are stable, React can:
- find the right old Fiber in the Map phase
- preserve component state more reliably
- distinguish "this item moved" from "this item is brand new"
When keys are unstable, React loses that advantage.
That is why using array index as a key can be dangerous for dynamic lists.
If you insert, remove, or reorder near the beginning of a list, index-based keys make many siblings appear to have changed identity. That can cause:
- unnecessary remounts
- reused state on the wrong visual item
- more placement work than needed
Index keys are not always catastrophic. For a truly static list that never reorders, they can be acceptable. But for real interactive collections, stable data IDs are far better.
11. What React intentionally does not optimize
The current child reconciler makes some tradeoffs on purpose.
It does not try to reuse across parent boundaries
If a node moves to a different parent or different structural level, React does not try to detect that as a preserved subtree move. It will remount in the new location.
It does not chase the global minimum edit script
That would be too expensive for normal UI rendering.
It is forward-only
There is even a comment in the source that the array algorithm does not optimize by searching from both ends, partly because Fibers do not have backpointers for that style of algorithm.
That is a very React-looking tradeoff:
- keep the structure simple
- optimize the common path
- accept that some uncommon reorder patterns are not globally optimal
12. Best practices that follow directly from the algorithm
Once you understand the reconciler, several React best practices stop feeling arbitrary.
Use stable keys from data
If your data already has an ID, use it as the key.
Keep subtree structure reasonably stable
If a piece of UI changes parent boundaries often, React will not preserve it the same way it can preserve sibling-level updates.
Treat type changes as identity changes
If you switch from one element or component type to another, React will not try to preserve the old subtree just because it sits in the same position.
Remember that reconciliation is sibling-local
React's best reuse story happens among siblings under the same parent, especially when the keys are stable.
13. The summary worth remembering
If you only want one compact mental model, keep this:
- single child updates go through
reconcileSingleElement - list updates go through
reconcileChildrenArray - React first tries the fast prefix path
- then it falls back to a Map for the remaining old children
lastPlacedIndexdecides whether a reused child stays or movesPlacementmeans either insertion or movement
That is why React reconciliation is fast in practice.
It is not a perfect tree-edit algorithm.
It is a carefully chosen set of heuristics, implemented in ReactChildFiber.js, that optimize for the update patterns UI code usually produces.
Reviewed by
DevDepth Editor
Editor and frontend engineering writer
DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.
Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.
Last editorial review: 2026-03-17