React Fiber
Before React 16, the rendering engine was a simple recursive stack. Once it started rendering, it couldn't stop. If a large tree took 200ms to process, the browser was blocked for all 200ms — no input, no animation, nothing.
Fiber is the rewrite that changed this.
At its core, React Fiber is a cooperatively scheduled rendering engine — a linked list tree and a small scheduler hiding in a trench coat. It's plain JavaScript, no magic, just a smarter way of breaking work into pieces.
What Fiber Is (And Isn't)
Fiber is not a speed improvement. React didn't get measurably faster at rendering individual components. What Fiber introduced is priority. The ability to say: this work is urgent, that work can wait.
Before Fiber, React had one speed: go. State changed, it called every function top to bottom, blocked the main thread until it was done, then committed to the DOM. This works fine for small apps. For large ones, every state change was a potential freeze.
After Fiber, React has a scheduler. It can pause, resume, or discard in-progress work based on what's more important.
What Is a Fiber?
A Fiber is a JavaScript object that represents a unit of work. React creates one Fiber for every component instance in your tree.
Each Fiber holds:
- What type of component it is (function, class, DOM element)
- The current props and state
- Pointers to its parent, first child, and next sibling
- A reference to the alternate Fiber (explained below)
- Work-in-progress flags (does this need to re-render? update the DOM?)
FiberNode {
type: Counter, // the component function
stateNode: null, // DOM node or class instance
child: → DisplayFiber,
sibling: → ButtonFiber,
return: → AppFiber, // "parent" in Fiber terminology
alternate: → ..., // the previous version of this fiber
flags: Update, // work that needs to be done
pendingProps: { ... },
memoizedProps: { ... },
memoizedState: { count: 3 },
}The Double Buffer
React always maintains two Fiber trees:
- Current tree — what is currently rendered on screen
- Work-in-progress tree — what React is building for the next render
The work-in-progress tree is React's working copy. If something more important comes in mid-render, React can abandon it, switch to the urgent work, and come back — or start over entirely. The current tree is untouched, so the user always sees a valid, complete UI.
After commit, these flip. The user sees the updated tree instantly.
Cooperative Scheduling
JavaScript functions run to completion. Once a function starts, it runs until it returns, yields (generators), or throws. There's no way to pause it midway. That's why the old React was blocking — once it started calling component functions, it was committed until the end.
Fiber works around this by not using a single recursive call. Instead, it breaks the tree traversal into small units, puts them on an event-queue-like structure, and processes them incrementally. The mental model is setTimeout(0):
// Old approach: one big blocking call
renderEntireTree(); // blocks for 200ms
// Fiber's approach: small chunks, yielding in between
doSomeWork();
setTimeout(0, doMoreWork); // let other things happen
setTimeout(0, doMoreWork); // keep goingReact aims to yield approximately every 5 milliseconds. That's not guaranteed — expensive functions can block longer — but it's the target. When it yields, it asks: is there more important work waiting? If yes, go do that. If no, keep going.
This also benefits everything else running in your app. The browser targets 60fps — that's a 16.6ms frame budget. By yielding regularly, Fiber lets CSS animations run, promises resolve, and other JS execute without being starved.
Two Phases of Rendering
Fiber splits every render into two phases:
Render phase (interruptible)
- React traverses the Fiber tree
- Calls your component functions
- Computes what changed (reconciliation / "diffing")
- This phase can be paused, restarted, or thrown away
Commit phase (not interruptible)
- React applies the changes to the DOM
- Runs
useLayoutEffectcallbacks (synchronously, before paint) - Runs
useEffectcallbacks (asynchronously, after paint) - Must complete in one go — DOM mutations can't be half-applied
Once React has figured out the full manifest of changes and starts committing to the DOM, it's in. A half-updated DOM is worse than a stale one.
This is why your component function might be called more than once during a single visual update in development (Strict Mode intentionally double-invokes renders to catch side effects).
What This Enables
| Feature | How Fiber makes it possible |
|---|---|
useTransition / startTransition | Marks state updates as "low priority" — Fiber can yield for them |
Suspense | Lets React pause a subtree mid-render and show a fallback |
| Concurrent features | Multiple in-progress trees can exist simultaneously |
| Error boundaries | Fiber can discard a work-in-progress subtree and show a fallback |
React.lazy | Code-split components can be loaded while Fiber waits |
The pre-Fiber engine had none of this. Once rendering started, the only way out was an error.
What This Means for Your Code
Component functions must be pure. Since the render phase can be restarted, your component might run two or three times before React commits anything. Side effects in render (directly writing to localStorage, directly calling APIs) will fire multiple times.
Strict Mode intentionally highlights this. In development, React runs the render phase twice on purpose. If you see double console.log in render — that's why.
Effects are for side effects. useEffect runs after commit, once, asynchronously. It's the right place for subscriptions, timers, and API calls.
useLayoutEffect is for library authors. It runs synchronously after DOM mutation but before the browser paints. That means it can block the entire commit phase. If you misuse it for API calls or anything slow, you've re-introduced the blocking behaviour Fiber was designed to eliminate. If you don't know you need it, you don't need it.
Practice what you just read.