Render and commit: why React calls your component more often than it paints
Render is React calling your components as pure functions to produce an element tree; commit is the only phase that mutates the DOM. Concurrent React can repeat or discard a render, so a side effect in render fires an unpredictable number of times — keep render pure.
The funnel team filed the ticket: impressions on the search results page were up 40% week-over-week with flat traffic, and conversion rate had “collapsed” accordingly. Marketing was already rewriting the quarter’s report. The frontend change that shipped that week looked innocent — the search page had been wrapped in startTransition so typing stayed responsive while results rendered. The culprit was one line that had sat in the codebase for two years: analytics.track("impression") called directly in the component body. In React 17 that was sloppy but invisible — every render committed exactly once, so render-count equaled paint-count. Under a transition, React starts rendering, throws the half-finished work away when the next keystroke arrives, and starts again — and every abandoned attempt fired the tracker. Nothing on screen was wrong. The DOM was perfect. The team had just never learned the difference between React calling their function and React updating the page, because for two years the two had coincided. StrictMode double-invokes render in development precisely to break that coincidence early — but the tracker was a no-op in dev, so nobody ever saw it.
A render is a function call, not a screen update
Every UI update in React moves through the same pipeline: trigger → render → commit. A trigger is either the initial mount or a state update (setState schedules a re-render; it does not perform one). The render phase is React calling your component functions. The return value is a tree of elements — plain, immutable JavaScript objects produced by JSX (react.createElement under the hood) that describe what the UI should look like: type, props, children. Creating elements is cheap — no DOM is touched, nothing is painted, nothing is measured. React renders the component whose state changed, then recursively renders its children, building a complete description of the next UI.
This is the part that trips people up: a “render” produces a description, and a description can be compared, cached, or thrown in the trash at zero cost to the user. The commit phase is where React takes the difference between the new description and what is currently on screen and applies the minimal set of real DOM mutations — appendChild, attribute updates, removals — in one synchronous, uninterruptible pass. After commit, the browser paints, then passive effects (useEffect) run. Layout effects (useLayoutEffect) run inside commit, after mutation but before paint — which is why they can measure the DOM without the user seeing a flicker, and why slow code there blocks paint directly.
Fiber: the unit of work that makes rendering interruptible
When you look at a React component, you see a function that runs and returns JSX — but internally, every component instance is backed by a fiber — a mutable node holding the component’s type, its current props, its hook state (a linked list — this is why hook order must be stable), and flags describing what changed. The render phase is a work loop over fibers: process one fiber, render its component, reconcile its children, move to the next. One fiber = one unit of work, and between units React can stop. For urgent updates React still runs the loop to completion synchronously, but inside transitions and other concurrent features it yields back to the browser roughly every 5 ms, so a long render of a 2,000-row tree no longer freezes typing for 300 ms — the keystroke handler runs between units of work.
The second structural trick is double buffering: React keeps the current fiber tree (what is on screen) and builds a separate work-in-progress tree during render. Commit is, at its core, swapping the pointer from current to work-in-progress. That separation is what makes discarding work safe: if a higher-priority update arrives mid-render, React abandons the work-in-progress tree — the current tree was never touched, so the screen is still consistent — and starts a fresh render from the new state.
▸Why this works
Why is commit synchronous and uninterruptible when render goes to such lengths to be interruptible? Because a half-applied DOM is user-visible corruption: a list with three of seven rows moved, a form whose label updated but whose input did not. Render works on descriptions, so interrupting it costs nothing anyone can see; commit works on the live DOM, so it must be atomic. React therefore pushes all the time-consuming, pausable work into render and keeps commit as a short burst of mutations. This is also why the rule is asymmetric: render must be pure, but effects — which run after commit — are exactly the place where impurity belongs.
Why render must be pure — and how StrictMode keeps you honest
Purity is not a style preference; it is the contract that makes the scheduler legal. Given the same props and state, your component must return the same elements and change nothing else — no mutating objects that existed before the call, no network requests, no writing to refs, no analytics.track. The reason is arithmetic: render may run zero, one, or many times per committed frame. A transition render can be discarded and restarted; a future offscreen render may run for UI that never becomes visible. A side effect in render therefore fires an unpredictable number of times — the Hook’s 40% phantom impressions were exactly this, side effects from renders that never committed.
function SearchResults({ items, query }) {
// Bug: render-phase side effect. Fires once per render *attempt* —
// including renders React discards — not once per paint.
analytics.track("impression", { query });
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
function SearchResultsFixed({ items, query }) {
// Effects run after commit — once per *committed* change of query.
useEffect(() => {
analytics.track("impression", { query });
}, [query]);
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}StrictMode is the enforcement tool: in development it deliberately invokes each component’s render twice (and re-runs effect setup/cleanup once on mount) so that any impurity produces a visible double — two logs, two requests, a mutated array growing twice. Pure components are unaffected by the double call, which is the whole test. Tradeoff: purity forbids doing work in the most “obvious” place and forces it into event handlers and effects, which feels bureaucratic on day one — but it is what buys interruptible rendering, render caching, and server rendering for free. Failure mode: teams that disable StrictMode “because it logs twice” are switching off the only alarm that fires before the production incident.
In development under StrictMode, a component's function body runs twice per render. Why does React do this on purpose?
During a transition, React has rendered about half of a large work-in-progress fiber tree when the user types another character. What happens to the half-finished work?
- 01Walk through what happens between a setState call and pixels changing on screen, naming both phases and what each is allowed to do.
- 02Why exactly does a side effect in the render phase cause production bugs under concurrent rendering, and how does StrictMode surface this in development?
React updates the UI in two strictly separated phases. The render phase is React calling your component functions: a trigger (mount or setState) schedules work, React invokes the component and its children, and the output is a tree of element objects — a pure description, no DOM touched. Each component instance is backed by a fiber holding its props, hook state, and change flags; the render phase is a work loop over fibers, one unit of work at a time, which is what lets concurrent React yield to the browser roughly every 5 ms inside transitions and keep typing responsive during a heavy render. React builds the next UI as a separate work-in-progress tree (double buffering), so when an urgent update preempts a transition render, the half-finished tree is simply thrown away and rebuilt — the current tree on screen was never touched. The commit phase is the opposite contract: one synchronous, atomic pass that applies the minimal DOM mutations and swaps the trees, followed by layout effects before paint and passive effects after. Because render can legally run zero, one, or many times per committed frame, it must be pure — a side effect in the component body fires once per render attempt, including discarded ones, which is how a tracker in render inflated impressions 40% the week transitions shipped. StrictMode double-invokes render in development to make exactly that impurity visible while it is still free to fix; side effects belong in event handlers and effects, which run a predictable number of times after commit. Now when you see analytics or network calls sitting directly in a component body, you’ll know exactly why they misfire under concurrent rendering — and where they belong instead.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.