Skip to content

How React renders

"Render" is one of the most used words in React and one of the least examined. Understanding what actually happens between a state update and the pixels changing on screen clarifies a lot: why components run when they do, why most re-renders cost nothing worth worrying about, and where real performance work should start. This chapter builds that mental model. The two chapters after it, Memoization and Code splitting, put it to work.

A render is a function call

When React "renders" a component, it calls your component function:

jsx
function Counter() {
  const [count, setCount] = useState(0)
  console.log('Counter rendered') // runs every time React calls this function

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

That's the whole trick. The function runs top to bottom, any calculations inside it run again, and it returns JSX describing what the UI should look like right now. Nothing about that involves the browser's DOM yet. A render produces a description; it doesn't touch the page.

That distinction matters because the two things have wildly different costs. Calling a JavaScript function and building a description is fast. Changing the real DOM, which triggers layout and paint work in the browser, is the expensive part. React's whole design leans on doing the cheap thing freely and the expensive thing as little as possible.

The three steps

A single "re-render" moves through three steps. The trigger is usually a state update, though the initial mount, a parent rendering above the component, and a new context value all run the same machinery.

  1. Render. React calls the component function whose state changed, and then, recursively, every component it returns. The result is a fresh virtual DOM: a lightweight in-memory description of that part of the UI.
  2. Reconciliation. React runs a diffing algorithm comparing the new virtual DOM against the previous one, working out exactly what changed between them.
  3. Commit. The differences, and only the differences, are applied to the real DOM. This is the part the user can actually see.

The course offers an analogy worth keeping. An architect updating a building first redraws the blueprint (render), then lists which changes the new blueprint actually requires (reconciliation), and only then does the construction crew touch the building (commit). Redrawing a blueprint is quick. Construction is where the time and money go, so the crew does the least work the list allows.

Rendering and reconciliation stay fast, and the commit stays scoped to what the diff found, which is why React feels quick by default. When something does get slow, the Profiler tells you which step to blame instead of leaving you to guess.

Every click on that Counter button runs all three: the log fires, React diffs the result, and the commit updates the button's text node and nothing else around it.

Rendering is recursive, and that's usually fine

When a component renders, React renders everything it returns, then everything those components return, all the way down each branch before moving to the next. A state change in a parent therefore re-renders all of its descendants by default, whether or not they receive the changed state as props.

That sounds wasteful until you separate the steps again. Those child renders are function calls producing descriptions. During reconciliation, React finds no changed attributes on any of them, so the commit has no DOM work to do for that subtree. The user-visible DOM work stays scoped to what actually changed. Re-rendering a subtree that commits nothing is the normal, healthy case, and for the vast majority of components it is far too cheap to measure.

It stops being fine in two situations: when a component does expensive work during its render, or when a subtree is so large that even cheap renders add up. Those are exactly the cases Memoization addresses. Resist the urge to reach for it before you have evidence, which brings us to measuring.

Measure before you optimize

React is already fast, and reconciliation already keeps DOM updates minimal. The course's advice is blunt: be able to measure a performance problem before applying a technique to fix it, and weigh every optimization against the readability of the code it complicates. Cleverness that nobody can maintain is a cost too.

Two tools cover most measuring needs:

  • The browser's Performance tab. Record a session, perform the slow interaction, stop, and inspect what ran and how long it took. Its CPU throttling (try a 4x or 6x slowdown) and network throttling let your fast development machine impersonate the slower devices and connections your users actually have. Problems invisible on your laptop show up immediately.
  • The React DevTools Profiler. A browser extension from the React team, living in its own Profiler tab. Same record-act-stop flow, but the output is React-shaped: which components rendered, why they rendered, how long each took, and how long the commit took. For diagnosing React apps specifically, it's usually the more direct tool.

StrictMode: a guardrail for development

StrictMode is a component you wrap around your app (or any subtree) to turn on development-only checks. It's the wrapper Setup puts around <App /> in main.jsx.

In development, StrictMode deliberately double-invokes your component functions, along with the other functions React expects to be pure: useState and useReducer initializers, the updaters you pass to a setter, reducers, and useMemo calculations. It runs each effect an extra time, setting it up, cleaning it up, and setting it up again, and it re-attaches ref callbacks. It also warns about deprecated APIs. In a production build it does nothing at all, so it never slows down or double-renders the app your users see.

Double-running sounds like sabotage until you see what it catches. A component function is supposed to be pure: same inputs, same output, no side effects during render. If rendering twice produces a different result than rendering once, the component was hiding a bug.

The course demonstrates this with a component that calls push on an array defined outside itself during render; the page looks fine until any state change re-runs the function and pushes a duplicate. Under StrictMode the duplicate appears on the very first load, and the fix (copy the array before modifying it, or move the work out of render) follows from it.

Purity covers your updaters

The same purity requirement covers the updater in setCount(c => c + 1), since that function runs twice too.

The effect re-run tests the same contract from the other side. Effects owns the cleanup rule and shows what a missing one looks like under StrictMode, when an uncleared setInterval leaves you counting by twos and leaking a timer per mount.

Seeing more bugs in development feels backwards at first, but that is the entire point. Every bug StrictMode surfaces is one that already existed and would otherwise have waited for production to introduce itself.

Version note

The effect behavior arrived in React 18: on mount, StrictMode runs each effect's setup, then its cleanup, then setup again. Older articles describe StrictMode as only double-rendering components. The course writes the wrapper as React.StrictMode; with named imports it's <StrictMode>.

The diffing step is cheaper than "compare two trees" suggests because React refuses to do a full tree comparison, which would be O(n³) in the general case. The heuristics: elements of different types tear down the old subtree entirely and rebuild, elements of the same type are kept and only their changed attributes patched, and list reconciliation is guided by keys, which is why keys must be stable identities.

React reaches for Object.is in the comparisons that gate work: bailing out of an update when you set state to the same value, checking dependency arrays, and memo's shallow props comparison. Two deeply equal objects with different identities fail all three. This is the thread that runs through the next chapter: a parent passing {} or an inline arrow function creates a fresh identity every render, which is harmless by default but defeats every identity-based optimization the moment you add one.

React's own docs group the first two steps into a single render phase: the diffing happens as React walks the tree element by element, so nothing waits for a complete description to be built first. A bailout can therefore stop partway down a branch. Splitting reconciliation out is a teaching convenience for describing what the diff does, and the two-phase render/commit model is the one the Profiler and the React source use.

That render phase is also, by design, allowed to be thrown away. React may start rendering, discard the work, and render again before committing, and concurrent features lean on this to keep the main thread responsive.

That's the deeper reason renders must be pure: React only guarantees that committed work happens exactly once. Anything a component does during the render phase (mutations, subscriptions, logging you rely on) may happen zero, one, or several times. StrictMode's double-invoke is a cheap simulation of that reality in development, and code that breaks under it is code concurrent rendering can break for real.

When the Profiler does show a real problem, match the tool to the step. Long render bars on a component doing heavy computation point to useMemo; a wide subtree re-rendering with nothing to commit points to memo; both live in Memoization. A slow initial load, where the problem is JavaScript arriving before any rendering can start, is a bundle problem rather than a render problem, and Code splitting is the lever.

Sometimes the fix is structural: moving state down to the component that uses it, or passing expensive subtrees as children so the stateful parent's re-render sees the same element objects and bails out of that branch, removes renders without any memoization at all.

JunoRenders are cheap, commits are precise A render means React calls your component function again to ask what the screen should look like. It compares the new answer with the old one and updates only the parts of the page that actually changed. When a parent renders, all its children render too, and that's normal and almost always fast.

Wrap your app in StrictMode while you build: it runs things twice on purpose in development to help you spot bugs early, and it switches itself off in the finished app.

JunoRenders are cheap, commits are precise Three steps: render calls your component functions and builds a new virtual DOM, reconciliation diffs it against the old one, commit applies only the differences to the real DOM. Parent renders cascade to children by default, and since most of those renders commit nothing, they cost almost nothing.

Before optimizing anything, record the slow interaction in the React DevTools Profiler and throttle your CPU to see what users see.

Keep StrictMode on: double-invoked renders expose impure components, and re-run effects expose missing cleanup like an uncleared setInterval.

JunoRenders are cheap, commits are precise Reconciliation is heuristic: type changes rebuild subtrees, same-type elements patch in place, keys drive list diffing, and the comparisons that gate work (state bailout, dependency arrays, memo) run on Object.is, so fresh object and function identities read as changes.

Render-phase work is discardable by contract, which is why purity matters and why StrictMode simulates the double execution concurrent rendering can produce.

Profile first, then match the fix to the phase: memoize expensive renders, split bundles for slow loads, or restructure with children so the work never happens at all.

Next up: Memoization, where referential equality decides what React can skip.