The Rendering Lifecycle
React is often described as a library that “updates the UI when state changes.” But how exactly does it do that efficiently? Understanding the rendering lifecycle—specifically the distinction between the Render Phase and the Commit Phase—is the key to optimizing performance and avoiding common pitfalls like infinite loops or unnecessary re-renders.
First Principles: The Physics of Rendering
Why does React exist? Why not just update the DOM directly?
The Cost of DOM Mutation
The browser’s DOM is slow to update because every change triggers a cascade of calculations:
- Recalculate Styles: The browser figures out which CSS rules apply.
- Reflow (Layout): The browser calculates the position and geometry of every element.
- Repaint: The browser paints pixels to the screen.
If you update the DOM 100 times in a loop, the browser might try to reflow 100 times. React solves this by batching updates and using a Virtual DOM.
The Virtual DOM (VDOM)
The Virtual DOM is a pure JavaScript object tree that mirrors the actual DOM.
- Accessing JS Objects: ~0.0001ms
- Accessing Real DOM: ~0.1ms (1000x slower)
React calculates changes in the cheap VDOM world and then applies the minimal set of changes to the real DOM in one go.
Phase 1: The Render Phase
The Render Phase is where React determines what changes need to be made.
- Trigger: A state update, parent re-render, or context change schedules a render.
- Calculation: React calls your component function.
- Reconciliation: React compares the new Virtual DOM tree returned by your component with the previous one.
Fiber Architecture
Since React 16, this phase is powered by Fiber. Fiber allows React to:
- Pause, abort, or prioritize work.
- Split rendering work into chunks (time-slicing).
- Prioritize user interactions (clicks/input) over background data fetching.
Phase 2: The Commit Phase
The Commit Phase is where the side effects happen. This phase is synchronous and cannot be interrupted.
- Apply Changes: React applies the diffs calculated in the Render Phase to the real DOM.
- Layout Effects:
useLayoutEffectruns synchronously after DOM mutations but before the browser paints. - Paint: The browser paints the new UI to the screen.
-
Passive Effects:
useEffectruns asynchronously after the paint.
[!NOTE] This module explores the core principles of Rendering Lifecycle, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.
1. Interactive: Render Cycle Visualizer
Explore how React moves through the phases when state changes. Click “Trigger Update” to start the cycle.
Component State
Current Status
Component Anatomy: The Lifecycle
Understanding the exact sequence of events is crucial. Here is the anatomical breakdown of a React component’s lifecycle:
- 1. Trigger: State or props update (e.g.,
setCount(1)). - 2. Render: React calls your component function.
- 3. Diff (Reconciliation): React compares the new Virtual DOM with the previous one.
- 4. Commit: React mutates the real DOM (synchronously).
- 5. Layout Effects:
useLayoutEffectruns (synchronously, blocking paint). - 6. Paint: The browser renders the pixels to the screen.
- 7. Passive Effects:
useEffectruns (asynchronously).
2. Batching & State Updates
Automatic Batching (React 18+)
React groups multiple state updates into a single re-render for better performance. This process is called batching.
Case Study: The Double Render Problem
Before React 18, asynchronous operations like promises or timeouts broke the batching mechanism.
| Scenario | React 17 Behavior | React 18 Behavior |
|---|---|---|
| Event Handler | Batched (1 render) | Batched (1 render) |
Promise .then() |
Not Batched (Multiple renders) | Batched (1 render) |
setTimeout |
Not Batched (Multiple renders) | Batched (1 render) |
| Native Event Listener | Not Batched (Multiple renders) | Batched (1 render) |
// Example: Asynchronous Batching
fetchData().then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 18: Safely batches these updates. Only ONE re-render happens.
});
Opting Out: flushSync
In rare scenarios, such as needing to synchronously read the DOM immediately after a state change, you can force a synchronous render using flushSync from react-dom.
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1);
});
// DOM is guaranteed to be updated here.
3. Optimizing the Render Cycle
Performance optimization in React revolves around preventing unnecessary renders or minimizing the cost of necessary ones. If a component’s render function is slow, it blocks the main thread, causing jank.
Referential Equality & Re-renders
React re-renders a component if its state or props change. By default, React checks object props using referential equality (Object.is). If a parent component re-renders, it creates new references for objects and arrays passed as props, triggering child re-renders even if the underlying data hasn’t changed.
React.memo
React.memo is a higher-order component that memoizes the rendered output. It skips re-rendering if the props (checked via shallow comparison) have not changed.
const ExpensiveComponent = React.memo(function({ data }) {
// Only re-renders if `data` prop changes reference
return <div>{slowCalculation(data)}</div>;
});
React.memo introduces its own comparison overhead. Reserve it for components demonstrably causing bottlenecks.
useMemo & useCallback
When passing objects or functions to memoized child components, you must stabilize their references using hooks.
useMemo: Caches the result of an expensive calculation.useCallback: Caches a function definition.
function ParentComponent() {
const [count, setCount] = useState(0);
// Stabilize the object reference across renders
const data = useMemo(() => ({ value: 'constant' }), []);
// Stabilize the function reference across renders
const handleClick = useCallback(() => console.log('Clicked'), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Update State</button>
<ExpensiveComponent data={data} onClick={handleClick} />
</div>
);
}
The “RDCP” Mnemonic
To remember the core phases, think of RDCP:
- Render (Calculate)
- Diff (Compare)
- Commit (Apply)
- Paint (Display)
4. Architectural Summary
- Render ≠ Update: Rendering is just calculating the diff.
- Virtual DOM: The lightweight copy used for diffing.
- Commit: When the DOM is actually touched.
- Side Effects: Belong in
useEffect, which runs after the paint.