What Is React Fiber? How React Works Under the Hood
So, React Fiber is the reconciler React has used since React 16: it turns your component tree into a tree of small units of work called fibers, so React can stop rendering partway through, handle something more urgent, and pick the work back up. I spent a chapter of Fluent React on it, and I’ve explained it on stage a bunch of times, usually with a counter and a whiteboard marker that’s running out of ink.
Before Fiber, React rendered an update in one go: it walked your whole tree recursively, and once it started it couldn’t stop until it was done, so a big update could freeze the input you were typing into. As I put it at Frontend Nation 2024, “UI updates are not created equal”, and typing into an input matters more than re-rendering a list nobody’s looking at yet. But like, how do you interrupt a recursive function halfway through and come back to it later?
TL;DR
- React Fiber is the reconciler React has used since React 16. It replaced the stack reconciler, which had to finish every update once it started.
- A fiber is a plain JavaScript object, one per component or element, holding its type, props, state, effects, priority and pointers to its parent, child and sibling.
- Those pointers turn the tree into something React can walk with a
whileloop, so it can stop after any fiber and resume later. - React keeps 2 trees, the one on screen and the one being built, and swaps them when the new one is finished: double buffering, borrowed from games.
- Rendering happens in 2 phases: a render phase that can be paused or thrown away, and a commit phase that always runs to the end.
- Lanes are bitmasks that mark how urgent each update is, which is how a keystroke gets to cut in front of a transition.
What is React Fiber?
React Fiber is the reconciler React has used since React 16, and a fiber is the unit of work it schedules: one JavaScript object for every component and host element in your app. The name covers 3 related things, and it helps to keep them apart:
| Term | What it is | Example |
|---|---|---|
| Fiber, the architecture | The reconciler that shipped with React 16 in September 2017 and still runs React 19 | The code in react-reconciler |
| A fiber | One object per component or element, reused across renders | The fiber for your <Counter /> |
| The fiber tree | Every fiber, linked by parent, child and sibling pointers | App > div > h1, span, button |
And here’s how a fiber relates to the things it gets confused with:
| Thing | What it is | How long it lives |
|---|---|---|
| React element | The object JSX gives you, describing what you want on screen | One render, then it’s garbage |
| Fiber | React’s record of a component instance: type, props, state, effects, priority, pointers | Across renders, updated in place |
| DOM node | The real thing in the browser | Until React removes it |
The virtual DOM is the older name for the whole idea of describing UI as objects and letting React work out the DOM changes. Fiber is the machinery that does that work today. Andrew Clark’s React Fiber Architecture notes are where most people first met the design, and Lin Clark’s A Cartoon Intro to Fiber from React Conf 2017 is still the friendliest explanation of why it exists. What I want to add here is the code: what React actually does today, line by line, on one small example, from the current React source.
Why React needed Fiber
Before React 16, the reconciler worked like a recursive function. Here’s a sketch of the shape (this is my simplification to show the problem, not React’s real code):
function reconcile(element) {
const children = render(element);
for (const child of children) {
reconcile(child); // the call stack is the only record of where we are
}
}
The problem is the call stack. JavaScript runs on the browser’s main thread, and a recursive walk keeps its place in the stack frames, so there’s no way to say “pause here, let the browser handle that keystroke, and come back to this exact component later”. Once an update started, it ran until the whole tree was done, and if the tree was big, typing into an input felt sticky because the keystroke had to wait behind a re-render of things nobody was looking at.
An update that can’t be interrupted makes every update as urgent as the slowest one.
What’s inside a fiber
Here’s the constructor from the React source, trimmed of the profiler and development-only fields (full file):
function FiberNode(tag, pendingProps, key, mode) {
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.refCleanup = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
}
The fields fall into 5 groups:
| Group | Fields | What they’re for |
|---|---|---|
| Identity | tag, key, type, stateNode |
What kind of thing this is (a function component, a div, a Suspense boundary), and for a host element, the real DOM node |
| Tree pointers | return, child, sibling, index |
The parent, the first child, the next sibling. These are what make the tree walkable without recursion |
| Data | pendingProps, memoizedProps, memoizedState, updateQueue |
The new props, the props and state from last time, and updates waiting to be applied. Your hooks live in memoizedState as a linked list |
| Work | flags, subtreeFlags, deletions, lanes, childLanes |
What has to happen to the DOM, and how urgent the pending work is here and below |
| The other copy | alternate |
This fiber’s twin in the other tree, more on that in step 4 |
I love that return is called return: it’s where the work goes back to when this fiber is done, the same way a function returns to its caller. Fiber is literally React rebuilding the call stack as data so it can hold on to it.
How Fiber renders a counter, step by step
Let’s grow one example the whole way through, the same counter I draw on stage:
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Welcome</h1>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Step 1: JSX becomes elements, and elements are throwaway
Every render, App returns a fresh tree of React elements. An element is a small object describing what you want, and in React 19 it’s tagged with Symbol.for("react.transitional.element") so React can tell it apart from any other object. Elements don’t hold state and they don’t survive the render, which is the whole reason React needs something that does.
Elements describe the screen you want, and fibers remember the screen you have.
Step 2: the fiber tree is linked by child, sibling and return
React keeps one fiber per element, and links them with 3 pointers. For the counter, it looks like this:
| Fiber | child |
sibling |
return |
|---|---|---|---|
App |
div |
none | the root |
div |
h1 |
none | App |
h1 |
none | span |
div |
span |
none | button |
div |
button |
none | none | div |
Notice how a parent only points at its first child, and that child points at its next sibling. The h1, span and button have no child fibers at all, btw: when an element’s only child is a string or a number, React DOM sets the text directly (shouldSetTextContent) and skips making a fiber for it. That’s what turns a tree into something you can walk with nothing but a variable holding “the fiber I’m on right now”.
A tree you can walk with a pointer is a tree you can stop walking at any point.
Step 3: the work loop does one fiber at a time
This is the loop React uses for urgent updates, verbatim (source):
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
And this is the one it uses for work that’s allowed to wait, also verbatim, minus a comment (source):
function workLoopConcurrentByScheduler() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
The only difference is !shouldYield(). React’s scheduler answers that by checking how long it’s been running, and with the defaults in the source today it gives the main thread back after about 5 milliseconds, so the browser can paint and handle your keystroke. workInProgress is a module-level variable, so when React comes back, the loop picks up at exactly the fiber it stopped on. Wild that the whole “concurrent React” idea comes down to one extra condition in a while loop.
Concurrent rendering is a loop that checks the clock after every fiber.
Step 4: beginWork goes down, completeWork comes back up
Each unit of work has 2 halves. performUnitOfWork calls beginWork, which renders the component and returns its first child, and when a fiber has no children left, completeUnitOfWork finishes it and moves sideways or up. Here’s the end of performUnitOfWork (source):
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
And the heart of completeUnitOfWork, trimmed of profiling and the error path (source):
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
next = completeWork(current, completedWork, entangledRenderLanes);
if (next !== null) {
workInProgress = next;
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
So for the counter, the order React visits things in is: begin App, begin div, begin h1, complete h1, begin span, complete span, begin button, complete button, complete div, complete App. Going down, React calls your components and compares their output against the existing fibers. Coming back up, completeWork prepares the DOM changes for that fiber and bubbles its flags into the parent’s subtreeFlags, so at the end React can skip any subtree where nothing changed.
Rendering goes down the tree and completing comes back up, and the loop can pause between any 2 steps.
Step 5: 2 trees, swapped at the end
React keeps 2 fiber trees: current, which is what’s on screen, and the work-in-progress tree it’s building. Each fiber’s alternate points at its twin in the other tree. The comment in createWorkInProgress says it better than I can (source):
// We use a double buffering pooling technique because we know that we'll
// only ever need at most two versions of a tree.
Double buffering comes from games and graphics: you draw the next frame off screen, and when it’s done you swap it in, so the player never sees a half-drawn frame. React does the same thing. When you click Increment, it builds the next tree off screen, and if a more urgent update comes in halfway, it can throw that work away and start again from current, because nothing on screen was touched. When the tree is finished, the commit ends with one line (source):
root.current = finishedWork;
And the work-in-progress tree becomes the screen. The old current tree becomes the next render’s scratch space, which is why React only ever needs 2.
Building the next screen off to the side is what makes it safe to stop, restart or throw away a render.
Step 6: lanes decide what’s urgent
Every update gets a lane, and a lane is one bit in a 31 bit number (source). Because they’re bits, a fiber’s lanes field can hold several pending priorities at once and React can check them with a bitmask, which is really fast. Some of the lanes, from most to least urgent:
| Lane | Used for |
|---|---|
SyncLane |
Discrete events, like a click or a keypress |
InputContinuousLane |
Continuous events, like dragging or scrolling |
DefaultLane |
Ordinary updates that don’t come from an event |
| Transition lanes | Updates you wrapped in startTransition |
IdleLane |
Work that can wait until nothing else is going on |
This is where Fiber pays off in code you write. If our counter also filtered a big list, you’d wrap the slow part in a transition:
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value); // urgent: the input updates right away
startTransition(() => {
setFilter(e.target.value); // can wait: the list can lag behind
});
}
The keystroke lands in an urgent lane and the filter in a transition lane, so if you type again before the list is done, React drops the half-built list render, handles the keystroke, and starts the list again with the newer value.
Lanes let React put a keystroke in front of a slow render without you writing a scheduler.
Step 7: the commit can’t be interrupted
Once the work-in-progress tree is complete, React commits it, and the commit always runs to the end in one go so you never see half an update. It happens in a few passes: one before React changes the DOM, one that changes it, and one for layout effects like useLayoutEffect, and then your useEffect callbacks run afterward as passive effects. The render and commit page on react.dev covers this from the outside, and in the source it’s commitBeforeMutationEffects, commitMutationEffects and commitLayoutEffects, then flushPassiveEffects.
Render can stop and start, and commit can’t, so the DOM only ever changes all at once.
What changed, step by step
| Step | What Fiber does | What it buys you |
|---|---|---|
| 1 | Elements describe the screen, fibers remember it | State survives between renders |
| 2 | Child, sibling and return pointers | A tree you can walk without recursion |
| 3 | A work loop that checks the clock | React can give the main thread back about every 5 ms |
| 4 | beginWork down, completeWork up |
Pause points between every fiber, and skipped subtrees |
| 5 | 2 trees and alternate |
Renders can be thrown away safely |
| 6 | Lanes | Urgent updates cut in front of slow ones |
| 7 | An uninterruptible commit | The screen never shows half an update |
What Fiber means for the code you write
You never touch a fiber directly, but the architecture shows up in a few rules that would feel arbitrary without it:
1. Keep your components pure when they render. The render phase can run a component, throw the result away and run it again, so a side effect inside render can happen twice or happen for a render that never reaches the screen. That’s why Strict Mode calls your components twice in development, and it’s the reason react.dev’s Keeping Components Pure page exists.
2. Put side effects in effects. Effects run after the commit, when the DOM is real and the render is final.
3. Use keys to tell React which fiber is which. React matches new elements to existing fibers by type and key, and a stable key is what lets a list item keep its state when the list reorders. react.dev’s page on preserving and resetting state is the long version.
4. Mark slow updates as transitions. useTransition and useDeferredValue are how you tell Fiber which work can wait, and it’s the cheapest performance win in modern React.
What I still don’t know
The internals keep moving. Everything I’ve quoted is from the main branch in September 2026, and there’s already a flag in there, enableThrottledScheduling, that would swap the 5 ms check for a loop that yields every 25 ms during transitions, on purpose, so animations can’t starve the render. It’s off by default today, and I genuinely don’t know whether it ships.
I’m also not sure how much of this stays visible once the React Compiler is memoizing everything for you. My guess is that the rules in the last section matter more with a compiler, since it assumes your components are pure, but that’s a guess.
Takeaways
- React Fiber is the reconciler React has used since version 16, and a fiber is one object per component that React keeps between renders.
- Parent, child and sibling pointers turn the tree into something a
whileloop can walk, pause and resume. - React builds the next tree off screen and swaps it in, so a render can be thrown away safely.
- The render phase can pause and restart, and the commit phase always finishes.
- Lanes are bitmasks for priority, and
startTransitionis how you put work in a slow lane. - Keep components pure, put side effects in effects, give list items stable keys, and mark slow updates as transitions.
If you want the whole thing, reconciliation through Server Components, it’s in Fluent React, my O’Reilly book. If you’d rather your team learned it in a day than a book, I teach a React internals workshop. If this made Fiber click for you, please share it with someone who’s still scared of it.
ok bye
Questions
What is React Fiber?
React Fiber is the reconciler React has used since React 16: it turns your component tree into a tree of small units of work called fibers, so React can stop rendering partway through, handle something more urgent, and pick the work back up. It replaced the stack reconciler, which had to finish a whole update once it started.
What is a fiber in React?
A fiber is a JavaScript object React keeps for every component and host element in your app. It holds the component's type, its props and state, the effects it needs to run, its priority, and pointers to its parent, first child and next sibling, which is what lets React walk the tree one fiber at a time.
What is the difference between React Fiber and the virtual DOM?
The virtual DOM is the idea of describing your UI as plain objects and letting React work out the real DOM changes. Fiber is how React actually does that work since React 16: the React elements your components return are compared against a tree of fibers, which hold state between renders and can be processed in pieces.
What is reconciliation in React?
Reconciliation is the process React uses to work out what changed between the last render and this one, so it only touches the parts of the screen that need to change. In React 16 and later the reconciler doing that work is Fiber.
What are the render phase and the commit phase in React?
The render phase is where React calls your components and works out what changed, and it can be paused, restarted or thrown away. The commit phase is where React applies those changes to the DOM and runs effects, and it always runs to the end in one go, so the screen never shows half an update.
Why does my React component render twice?
In development, Strict Mode calls your components twice on purpose to catch impure rendering, because Fiber's render phase is allowed to run a component more than once before anything reaches the screen. A component that gives the same output for the same props and state is unaffected.
What are lanes in React?
Lanes are how React tracks the priority of an update. Each lane is a bit in a 31 bit number, so a fiber can carry several pending priorities at once: SyncLane for discrete events like clicks, InputContinuousLane for things like dragging, DefaultLane for ordinary updates, transition lanes for work marked with startTransition, and IdleLane for work that can wait.
Does React 19 still use Fiber?
Yes. React 19 renders through the same Fiber reconciler, and features like transitions and Suspense depend on it. The code quoted in this post is from the main branch of the React repository in September 2026.
Written by me, Tejas Kumar, an AI Engineer at IBM based in Berlin. Read everything else I have written, or go to Fluent React, my O'Reilly book on how React works inside, the talks I give at conferences, and ConTejas Code, my podcast.