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:

  1. Recalculate Styles: The browser figures out which CSS rules apply.
  2. Reflow (Layout): The browser calculates the position and geometry of every element.
  3. 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.

  1. Trigger: A state update, parent re-render, or context change schedules a render.
  2. Calculation: React calls your component function.
  3. 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.
graph TD A[State Change] --> B{Render Phase} B --> C[Call Component Function] C --> D[Generate New VDOM] D --> E[Diff with Old VDOM] E --> F{Changes Found?} F -->|No| G[End (No DOM Update)] F -->|Yes| H[Schedule Commit] style B fill:var(--accent-main),stroke:var(--bg-card),color:var(--text-main) style H fill:var(--accent-main),stroke:var(--bg-card),color:var(--text-main)

Phase 2: The Commit Phase

The Commit Phase is where the side effects happen. This phase is synchronous and cannot be interrupted.

  1. Apply Changes: React applies the diffs calculated in the Render Phase to the real DOM.
  2. Layout Effects: useLayoutEffect runs synchronously after DOM mutations but before the browser paints.
  3. Paint: The browser paints the new UI to the screen.
  4. Passive Effects: useEffect runs 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

Count: 0

Current Status

Idle
Waiting for interaction...
1
Render
2
Commit
3
Paint
4
Effect

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: useLayoutEffect runs (synchronously, blocking paint).
  • 6. Paint: The browser renders the pixels to the screen.
  • 7. Passive Effects: useEffect runs (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>;
});
Architectural Note: Do not optimize prematurely. The Virtual DOM diffing process is highly optimized. Applying 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.