Table of Contents
- What is Reconciliation?
- The Need for Reconciliation: Why Not Update the DOM Directly?
- A High-Level Overview: How React Updates the DOM
- React’s Reconciliation Algorithm: The Diffing Process
- List Reconciliation: The Critical Role of Keys
- React Fiber: The Evolution of Reconciliation
- Optimizing Reconciliation: Best Practices
- Common Misconceptions
- Conclusion
- References
What is Reconciliation?
Reconciliation is the process by which React compares the previous version of the Virtual DOM (vDOM) with the new version (after state/props change) to identify the minimal set of changes needed to update the actual DOM.
In simpler terms: When your component re-renders (due to setState, useState, or prop changes), React generates a new Virtual DOM tree. It then compares this new tree with the old one to find differences (“diffs”). Finally, it applies only those differences to the real DOM—this step is called committing.
The key insight here is that DOM operations (like adding/removing elements) are computationally expensive. Reconciliation minimizes these operations by ensuring only necessary changes are applied.
The Need for Reconciliation: Why Not Update the DOM Directly?
You might wonder: Why not skip the Virtual DOM and directly update the DOM when state changes? The answer lies in performance:
- DOM operations are slow: Manipulating the DOM involves reflow/repaint cycles in the browser, which are costly. For example, updating a single attribute on a DOM node triggers layout recalculations for all its children.
- Batched updates: React batches multiple state changes into a single re-render, reducing the number of DOM updates.
- Declarative API: React’s declarative model lets you describe what the UI should look like, not how to update it. Reconciliation handles the “how” under the hood.
A High-Level Overview: How React Updates the DOM
Before diving into the details, let’s outline the 3-step process React follows when state or props change:
- Render: The component re-renders, generating a new Virtual DOM tree (a lightweight JavaScript object representation of the DOM).
- Reconciliation (Diffing): React compares the new Virtual DOM tree with the previous one to find differences (diffs).
- Commit: React applies the diffs to the actual DOM, updating only the changed parts.
React’s Reconciliation Algorithm: The Diffing Process
At the heart of reconciliation is React’s diffing algorithm—a set of rules that determine how React compares two Virtual DOM trees. The algorithm is designed to be fast (O(n) time complexity, where n is the number of nodes) while making reasonable assumptions about how UIs change.
4.1 Comparing Root Elements
React starts by comparing the root elements of the old and new Virtual DOM trees. The behavior differs based on the type of the root element:
-
Different element types: If the root elements are of different types (e.g.,
<div>vs.<span>), React assumes the entire subtree has changed. It tears down the old tree (unmounting all components) and builds a new tree from scratch (mounting new components).Example:
// Old Virtual DOM <div className="old">Hello</div> // New Virtual DOM <span className="new">Hello</span>React will unmount the
<div>and mount the<span>, discarding all child nodes of the<div>. -
Same element type: If the root elements are of the same type, React updates the element’s attributes/props and recursively compares its children.
Example:
// Old Virtual DOM <div className="old" style={{ color: 'red' }}>Hello</div> // New Virtual DOM <div className="new" style={{ color: 'blue' }}>Hello</div>React updates the
classNameandstyleattributes but keeps the underlying DOM node intact.
4.2 Updating Attributes and Props
For elements of the same type, React updates only the changed attributes. For example:
- For HTML elements (e.g.,
<div>,<img>), React updates properties likeclassName,src, or event handlers (e.g.,onClick). - For custom components, React passes the new props to the component and triggers a re-render (unless optimized with
React.memoorshouldComponentUpdate).
4.3 Recursively Comparing Children
After updating the root element, React recursively compares the children of the old and new trees. By default, React iterates over both lists of children and compares them element-by-element.
Example:
// Old Children
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
// New Children
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
React compares the first two <li> elements (unchanged) and adds the third <li>.
List Reconciliation: The Critical Role of Keys
The naive approach to comparing children (iterating element-by-element) works well for most cases, but it breaks down for lists—especially when items are added, removed, or reordered. This is where the key prop becomes essential.
5.1 The Problem with Naive List Diffing
Consider a list where items are reordered:
// Old List
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
// New List (items reordered)
<ul>
<li>Banana</li>
<li>Apple</li>
</ul>
Without keys, React compares the first <li> (“Apple” vs. “Banana”) and updates its content, then compares the second <li> (“Banana” vs. “Apple”) and updates its content. This works, but it’s inefficient—React is updating two nodes when only their order changed.
For dynamic lists (e.g., todo lists with add/remove/reorder), this inefficiency becomes worse. For example, adding an item to the start of a list would cause React to update every subsequent item (since their positions have shifted), even if their content is unchanged.
5.2 How Keys Solve This
Keys help React uniquely identify items in a list across re-renders. When keys are provided, React uses them to:
- Determine which items have been added, removed, or reordered.
- Reuse existing DOM nodes for items with the same key, avoiding unnecessary re-creation.
Example with keys:
// Old List with Keys
<ul>
<li key="apple">Apple</li>
<li key="banana">Banana</li>
</ul>
// New List (reordered) with Keys
<ul>
<li key="banana">Banana</li>
<li key="apple">Apple</li>
</ul>
With keys, React recognizes that “banana” and “apple” are the same items in a new order. It simply reorders the existing DOM nodes instead of updating their content.
5.3 Best Practices for Keys
-
Unique among siblings: Keys must be unique only within the list, not globally.
-
Stable: Keys should not change between re-renders (e.g., avoid using
Math.random()or array indices for dynamic lists). -
Avoid array indices: Using
indexas a key works for static lists (no reordering/adding/removing), but for dynamic lists, it causes bugs (e.g., incorrect state retention when items are reordered).Bad:
{todos.map((todo, index) => ( <TodoItem key={index} todo={todo} /> // Risky for dynamic lists! ))}Good:
{todos.map(todo => ( <TodoItem key={todo.id} todo={todo} /> // Use a unique, stable ID ))}
React Fiber: The Evolution of Reconciliation
Prior to React 16, reconciliation was handled by the stack reconciler—a recursive algorithm that couldn’t pause or prioritize work. This led to performance issues in large apps, as long-running reconciliation blocked the main thread (causing jank in animations or user interactions).
React 16 introduced Fiber—a complete reimplementation of the reconciliation engine designed to address these limitations.
6.1 What is Fiber?
Fiber is a reconciliation engine that reimplements the core algorithm. It’s not a new feature for developers but a foundational change to how React works under the hood. The term “Fiber” refers to the data structure used to represent work units (Fiber nodes).
6.2 Goals of Fiber
Fiber was built to enable:
- Incremental rendering: Break down reconciliation into smaller chunks and spread them over multiple frames.
- Priority-based scheduling: Assign priorities to different types of updates (e.g., animations get higher priority than data fetching).
- Pause, abort, or reuse work: Pause reconciliation to let the main thread handle user input or animations, then resume later.
- Better error boundaries: Gracefully handle errors in rendering without crashing the entire app.
6.3 How Fiber Works: Incremental Rendering
The stack reconciler used a recursive approach, which was synchronous and couldn’t be interrupted. Fiber replaces this with an iterative, linked-list-based approach that represents work as a sequence of Fiber nodes.
Each Fiber node corresponds to a component and tracks:
- The component’s type and props.
- Pointers to child, sibling, and parent Fiber nodes (forming a tree structure).
- The current state of work (e.g., “not started,” “in progress,” “completed”).
Fiber splits reconciliation into two phases:
-
Render Phase (Reconciliation):
- React traverses the Fiber tree, comparing old and new Virtual DOM nodes.
- It marks changes (e.g., “update this node,” “delete that node”) and schedules work.
- This phase is interruptible: React can pause, abort, or resume work here.
-
Commit Phase:
- React applies the changes marked in the Render phase to the actual DOM.
- This phase is synchronous and不可中断 (to avoid inconsistent UI states).
Optimizing Reconciliation: Best Practices
To ensure reconciliation runs efficiently, follow these best practices:
- Use stable keys for lists: As discussed, keys help React reuse DOM nodes. Avoid indices for dynamic lists.
- Memoize components with
React.memo: For functional components,React.memoprevents re-renders if props haven’t changed.const MyComponent = React.memo(({ name }) => <div>{name}</div>); - Memoize expensive values with
useMemo: Cache the results of expensive calculations to avoid re-computing them on every render.const sortedList = useMemo(() => sortItems(items), [items]); // Only re-sorts if items change - Memoize callbacks with
useCallback: Prevent unnecessary re-renders of child components by memoizing callbacks passed as props.const handleClick = useCallback(() => console.log("Clicked"), []); // Stable reference - Avoid anonymous functions in props: Anonymous functions create new references on every render, triggering unnecessary re-renders in child components.
// Bad <Child onClick={() => doSomething()} /> // Good (use useCallback) <Child onClick={handleClick} /> - Flatten component trees: Deeply nested component trees increase the work of reconciliation. Keep trees shallow when possible.
Common Misconceptions
-
“React re-renders the entire app on every state change.”
False. React starts re-rendering from the component that triggered the state change and recurses down, but optimizations likeReact.memoandshouldComponentUpdatestop propagation. -
“Keys need to be unique globally.”
False. Keys only need to be unique among siblings in the same list. -
“The Virtual DOM is always faster than direct DOM updates.”
False. For trivial apps, direct DOM updates might be faster. The Virtual DOM shines in complex apps where batching and diffing reduce unnecessary DOM operations.
Conclusion
React’s reconciliation algorithm is a masterpiece of engineering that enables the framework’s declarative, performant UI updates. By understanding how the diffing algorithm works, the role of keys, and the Fiber architecture, you can write apps that leverage React’s strengths while avoiding common pitfalls.
Remember: Reconciliation is optimized for the way UIs typically change, but it’s not magic. Following best practices like using stable keys, memoizing components, and keeping component trees shallow will ensure your app remains responsive even as it scales.