Reconciler and Diff Algorithm Source Code Walkthrough
React's Reconciler is the brain of the Fiber architecture. It receives a new React element tree, compares it with the existing Fiber tree, determines which nodes need to be created, updated, or deleted, and marks corresponding Fiber nodes with effect flags. Understanding how the reconciler works means understanding how React propagates virtual DOM changes to the real DOM at minimum cost.
The Reconciler Entry Point: reconcileChildFibers
Each time beginWork processes a Fiber node and needs to handle that node's children, it calls either reconcileChildFibers (update scenario) or mountChildFibers (initial mount scenario). These two functions are actually two instances produced by the same factory function:
// packages/react-reconciler/src/ReactFiberReconciler.js
// Both functions come from the same factory; the only difference is shouldTrackSideEffects
export const reconcileChildFibers = createChildReconciler(true);
export const mountChildFibers = createChildReconciler(false);
The shouldTrackSideEffects parameter is crucial. When it is true (update scenario), the reconciler marks Fiber nodes with side-effect flags like Placement (needs insertion) and Deletion (needs removal). When it is false (initial mount), these flags are not marked โ because during the initial mount the entire tree is new, and React will insert the root node into the DOM atomically at final commit without needing to mark each node individually.
// packages/react-reconciler/src/ReactChildFiber.js (simplified)
function createChildReconciler(shouldTrackSideEffects) {
function placeSingleChild(newFiber) {
// Only mark Placement if tracking side effects and the node is newly created (no alternate)
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
function deleteChild(returnFiber, childToDelete) {
if (!shouldTrackSideEffects) return;
// Add the node to be deleted to the parent's deletions list
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
// ... other helper functions
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
// Core dispatch logic
// Routes to different handlers based on the type of newChild
}
return reconcileChildFibers;
}
The Three Core Diff Rules
React's Diff algorithm is built on three rules. These rules did not arise from thin air โ they are engineering decisions made by the React team based on real-world Web application usage patterns.
Rule One: Different Type Means Destroy and Recreate
If the types of the new and old nodes differ, React makes no attempt to reuse anything. It deletes the old node and its entire subtree, then creates a new node from scratch.
// Type comparison in beginWork (simplified)
function updateElement(returnFiber, current, element, lanes) {
const elementType = element.type;
if (current !== null) {
if (current.elementType === elementType) {
// Same type โ reuse
const existing = useFiber(current, element.props);
existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
return existing;
}
// Different type: current is added to deletions, new Fiber is created
}
// Create a brand new node
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
}
Why not attempt "smart" reuse when node types differ? Because when the type changes (e.g., from <div> to <section>, or from ComponentA to ComponentB), the internal state is completely invalidated. Forcing reuse would only cause state contamination and bugs that are nightmares to track down โ it is cleaner to start fresh.
Rule Two: Same Type DOM Node โ Update Attributes Only
When the new and old nodes at the same position share the same type (e.g., both are <input type="text">), React reuses the existing DOM node and only updates the attributes that changed.
// packages/react-reconciler/src/ReactFiberCompleteWork.js (simplified)
function updateHostComponent(current, workInProgress, type, newProps) {
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
// Same props reference (memo scenario) โ no update needed
return;
}
// Compare new and old props, generate list of attributes to update
const updatePayload = prepareUpdate(
current.stateNode, // The real DOM node
type,
oldProps,
newProps,
);
// Save the update payload on the work-in-progress node
// The commit phase will use this payload to update the real DOM
workInProgress.updateQueue = updatePayload;
if (updatePayload) {
markUpdate(workInProgress);
}
}
prepareUpdate iterates over new and old props to find differences. For event listeners, it identifies them by the on prefix and replaces handler functions during updates. For style, it deeply compares every property of the style object.
Rule Three: Same Type Component โ Update Props and Continue Rendering
For components of the same type (function or class components), React reuses the existing Fiber node, passes the new props into the component, and re-executes rendering.
// packages/react-reconciler/src/ReactFiberBeginWork.js (simplified)
function updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes) {
// Call the function component itself with new props
let nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
// Reconcile children
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
Single Child Reconciliation
When there is only one new child element, the reconciliation logic is relatively straightforward:
// packages/react-reconciler/src/ReactChildFiber.js (simplified)
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
const key = element.key;
let child = currentFirstChild;
// Iterate existing child node linked list, looking for a reusable node
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (child.tag === Fragment ? element.type === REACT_FRAGMENT_TYPE
: child.elementType === elementType) {
// Both key and type match โ reuse this node
// Delete all other sibling nodes
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.return = returnFiber;
return existing;
}
// Key matches but type does not โ delete this and all subsequent nodes
deleteRemainingChildren(returnFiber, child);
break;
} else {
// Key does not match โ delete this node and keep looking
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// No reusable node found โ create a new one
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
A detail worth noting: when a node with a matching key but mismatched type is found, React immediately calls deleteRemainingChildren (deleting the current node and all subsequent siblings), then breaks out of the loop. This is because per Diff Rule One, different types must be rebuilt; and since keys are unique among siblings, we already know no later node will match either, so there is no need to continue searching.
Multiple Children Reconciliation: The Two-Pass Algorithm
List diffing is the most complex part of the reconciliation algorithm. When the new children are an array, React uses a two-pass traversal strategy:
// packages/react-reconciler/src/ReactChildFiber.js (core logic simplified)
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
let resultingFirstChild = null; // First node of the new linked list
let previousNewFiber = null; // Last processed new node
let oldFiber = currentFirstChild; // Current old node pointer
let lastPlacedIndex = 0; // Last confirmed-in-place index in the DOM
let newIdx = 0; // Current index in the new array
let nextOldFiber = null;
// ===== Pass 1: Sequential comparison, stop when a key mismatch is found =====
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// Attempt to reuse the old node
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
// Key mismatch โ stop Pass 1
if (oldFiber === null) oldFiber = nextOldFiber;
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// A different old node was used โ delete the original old node
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// ... build new linked list
oldFiber = nextOldFiber;
}
// Handle remaining cases after Pass 1 ends
if (newIdx === newChildren.length) {
// New elements exhausted โ delete remaining old nodes
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
if (oldFiber === null) {
// Old nodes exhausted โ create all remaining new elements
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
// ... add to new linked list
}
return resultingFirstChild;
}
// ===== Pass 2: Handle out-of-order nodes =====
// Put remaining old nodes into a Map, keyed by key (or index)
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// Reused an old node from the Map โ remove from Map to prevent erroneous deletion
existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
}
}
if (shouldTrackSideEffects) {
// Remaining old nodes in the Map have no corresponding new nodes โ delete them all
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
placeChild: The Minimum-Move Algorithm
The placeChild function in Pass 2 determines whether a node needs to move:
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) return lastPlacedIndex;
const current = newFiber.alternate;
if (current !== null) {
// This is a reused node โ check its position in the old list
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// Old position is to the left of already-stable nodes โ needs to move right
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// Old position is to the right of already-stable nodes โ no move needed
return oldIndex;
}
} else {
// Newly created node โ needs insertion
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
This algorithm has a well-known performance trap: moving the last node to the first position. For example, [A, B, C, D] becoming [D, A, B, C] causes the algorithm to move A, B, and C three times rather than moving D once. This is because D's old index (3) is greater than all other nodes, so the algorithm concludes that A, B, and C all need to move rightward. This is precisely why keys should not use array indices โ when list order changes, using indices as keys causes a flood of unnecessary DOM operations.
React 19 Reconciliation Optimizations
React 19 introduced several optimizations at the reconciliation algorithm level. One of them is early bailout based on subtreeFlags: if a subtree's subtreeFlags and childLanes are both zero, it means the subtree has no changes at all and reconciliation of that subtree can be skipped entirely.
// React 19 early exit optimization in beginWork
function beginWork(current, workInProgress, renderLanes) {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasLegacyContextChanged()) {
didReceiveUpdate = true;
} else {
// Props unchanged โ check for pending work
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
if (!hasScheduledUpdateOrContext) {
didReceiveUpdate = false;
// Reuse directly โ skip reconciliation
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
}
}
}
// ...
}
The reconciler is the core battlefield of React performance optimization. Understanding how it works is not merely satisfying technical curiosity โ it is the foundation for writing high-performance React code. Using key correctly, avoiding unnecessary type changes, organizing component hierarchies thoughtfully โ all of these are rooted in a deep understanding of the reconciliation algorithm.