JavaScript

Why React re-renders: understanding the Fiber work loop

Most React performance problems are really scheduling problems. A practical look at Fiber, reconciliation, and what actually happens after setState.

· 4 min read

Updated with images and a diagram.
Editorial cover: a component tree where one setState schedules a single subtree to re-render A component tree hangs from a root node. The right-hand subtree is highlighted in crimson with a teal pulse ring and a setState label, the left subtree stays faint and is marked skipped, and a badge notes that the render phase can be paused while the commit phase cannot. STATE CHANGED IN ONE COMPONENT SCHEDULING, NOT RENDERING setState() props unchanged · skipped re-renders render: pausable commit: not The interesting question was never how a component renders. It is when React decides the work should happen at all.

Ask most developers what React does and they'll tell you it renders components. Which is true, and also exactly why so many performance conversations end up in completely the wrong place.

Modern React is less a rendering library than a scheduling system. The interesting question was never how a component renders; it's when React decides the work should happen, and what it's willing to throw away to keep the page feeling alive.

The mental model most of us start with

The original React model was refreshingly simple. State updates, React walks the component tree, anything that needs updating gets reconciled, the DOM changes. Done.

For a small app, that works perfectly. For a large one it has a nasty edge: rendering can't be interrupted. A sufficiently expensive update will sit on the main thread long enough for the user to feel it - the dropped frame, the input that lands half a second late. The problem was never correctness. It's responsiveness, which is a much harder thing to fix.

Enter Fiber

React Fiber exists to solve precisely that.

Instead of treating a render as one big indivisible lump of work, React breaks it into small units that can be paused, resumed, reprioritised, or abandoned entirely. Every component in the tree is represented internally as a Fiber node.

When an update comes in, React builds a work-in-progress tree and processes it incrementally. If something more urgent shows up mid-render, React can down tools, deal with it, and pick the original work back up afterwards. That's the whole trick behind staying responsive while a lot of work is queued. Rendering stopped being a thing that happens and became a thing that gets scheduled.

Rendering is not committing

The single most common React performance misconception is treating rendering and DOM updates as the same event. They aren't.

React's work splits into two phases:

  • Render phase: work out what should change.
  • Commit phase: apply those changes to the DOM.

The render phase can be interrupted. The commit phase cannot.

Diagram showing setState feeding an interruptible render phase that builds a work-in-progress fiber tree and can pause, resume, or discard work, then handing the final result to an atomic commit phase that mutates the DOM and paints.

That one distinction explains a surprising amount of otherwise baffling behaviour. A component can render several times without a single pixel changing on screen, because React is exploring possibilities, and only the final committed result ever reaches the browser.

Why React sometimes throws work away

Developers are often a bit affronted to learn that React will do work and then bin it. This is deliberate, and it's a feature.

Picture a search box where someone's typing quickly. Each keystroke schedules an update, and the expensive part (filtering and rendering a big result list) is marked non-urgent:

import { useDeferredValue } from "react";

const query = useDeferredValue(rawQuery); // "rea" → "reac" → "react"
const results = useMemo(() => filter(data, query), [query, data]);

These updates arrive across separate keystrokes rather than in one tick, so React can't simply batch them out of existence. And there's genuinely no point fully rendering the list for "rea" once "reac" has already landed; it's a move you'd want to take back the instant you'd played it.

Because the deferred render is interruptible, the scheduler can abandon the lower-priority work the moment a newer keystroke turns up - and get on with producing the latest result instead. Seen this way, "wasted renders" aren't always waste. Quite often they're the scheduler doing exactly the job it was designed to do.

Most React performance issues aren't React issues

When a team goes hunting for frontend performance problems, React tends to get collared first. In practice, the bottleneck is usually somewhere else entirely:

  • Large datasets rendered when they didn't need to be.
  • Far too many DOM nodes.
  • Expensive layout calculations.
  • Memoisation boundaries drawn in the wrong places.
  • Components subscribed to far more state than they actually use.

More often than not, React is doing precisely what it was told to do. The application is just asking it for an absurd amount of work and then acting surprised when it obliges.

Optimise less, understand more

useMemo, useCallback and memo are useful tools. They're also reached for constantly, by reflex, before anyone has worked out what the actual problem is, which is paying an insurance premium on a risk nobody has measured.

The best optimisation is understanding why React thinks the work needs doing in the first place, the same habit that pays off one layer down in the engine itself. Once the work loop makes sense, a lot of performance problems get much easier to reason about. You stop asking why a component rendered, and start asking why React scheduled that work. That's almost always where the answer's been hiding.

Further reading

Human (you) in the loop

New writing, now and then

Occasional notes on platform engineering, building dependable software and that constant buzz word we doom scroll past on LinkedIn! No cadence promised.

Prefer a reader? Subscribe via RSS.

← All Articles

Jump to

32 articles