The interesting part of React starts after your component returns.
Most React topics focus on the parts we touch every day: components, props, state, hooks and JSX. Fiber sits somewhere underneath all of that.
You do not need to understand Fiber to build a React application. In fact, React works very hard to make sure you do not have to think about it.
But at some point in my React journey, knowing how to use React stopped being enough. I wanted to know what actually happened after a component returned its JSX.
How does React walk a large component tree? How does it remember where it is? Why does it have a render phase and a commit phase? How can one update be treated as urgent while another can wait? And why did React need to rewrite such a fundamental part of itself to make that possible?
That trail leads directly to Fiber. Fiber is not another public React API. It is the internal architecture that gave React much more control over its own rendering work. In this blog post, I want to follow the path I originally took when I asked myself how Fiber worked: start with the limitation of the old reconciler, pull Fiber apart as a data structure, then follow a piece of work through reconciliation, scheduling, render and commit. Let’s dig into it.
1. Why the old stack reconciler became a problem
Imagine an ordinary component tree. App renders a Dashboard; the dashboard renders a chart and a feed; the feed renders a row of items. React has to walk through the relevant parts of that structure, work out what each component returns, and compare the result with the current tree so it knows what can stay and what needs to change.
Before Fiber, React’s reconciler was organised around recursive traversal (React kept moving down into each child, then came back up when it reached the end of a branch). Conceptually, it was simple:
- Process a component.
- Call into its child.
- Keep descending until the branch ends.
- Return through the JavaScript call stack and visit the next sibling.
The runtime naturally remembered where every function needed to return. That worked well as long as React finished the whole walk in one go. It became a problem when React needed to pause.
Once a deep recursive traversal had entered the call stack, React could not easily say: I have done enough rendering for now. I will preserve this exact cross-component position and continue later.
- Synchronous main-thread work
The browser does not care that React has a large tree to reconcile. It still has input to handle, styles to calculate, layout to perform and pixels to paint. On a 60 Hz display, frames are about 16.7 milliseconds apart, but React does not own that whole interval. Long uninterrupted JavaScript competes with everything else on the main thread. That thread can only work through one task at a time, so while React keeps it busy, input handling, layout and painting have to wait for their turn.
- Priority
Not all UI work deserves equal urgency. Keeping a text input responsive matters more in the moment than finishing a large background results panel. An indivisible recursive traversal gave React little room to stop and reconsider that order after the work had begun.
So the problem was not recursion itself. The problem was that React did not own the traversal stack. It could start the walk and finish it, but it could not easily pause at a specific point in the component tree, save its place, continue later, or discard unfinished work when a more important update made it outdated.
Fiber changed the ownership of that state.
Read diagram transcript
- The old reconciler traverses App, Content and Item through one nested JavaScript call chain. React has no saved cross-component pause point inside that chain.
- Fiber stores nextUnitOfWork explicitly. App, Content and Item become separate units that the work loop can visit one at a time.
- After a completed unit, concurrent rendering can check whether it should yield. If it yields, the browser can process input or paint before React resumes.
- A Fiber unit still runs to completion once it starts. Fiber creates boundaries between units, but it cannot stop a component midway through its JavaScript.
The historical Fiber architecture notes described the goal clearly:
The details evolved, but that architectural goal is still the right doorway into the subject.
One caveat matters from the beginning: React can yield between Fiber units. It cannot suspend an arbitrary component halfway through a long calculation. Component code still runs on the normal JavaScript stack. Fiber controls when React moves from one component to the next, but it cannot stop a component halfway through its JavaScript.
2. What’s Fiber and how does a Fiber object look?
The simplest way I now think about Fiber is this:
A Fiber is a record React keeps for one place in the UI tree. It helps React remember what it is working on and where it should go next.
Imagine React has reached a heavy ResultsList component and needs to pause before moving into its children. To continue later, React needs to remember which component it was handling, its inputs and state, its parent, its first child and its next sibling. It also needs to know whether that part of the tree still has work waiting.
A Fiber holds that information. Each Fiber represents one position in the rendered tree. React keeps these records and reuses them as the UI changes.
This also makes the phrase unit of work easier to understand. During a render, React handles one Fiber, then chooses which Fiber to visit next. One Fiber is one manageable step in that walk through the tree.
It helps to separate two related structures:
- A React element is a short description of what should appear. JSX creates these descriptions.
- A Fiber is React’s internal work record. React uses it to process that description, connect it to the tree and track work over time.
My handwritten notes used inheritance to ask, What extra information would React need beyond an element? The question was useful, but inheritance is not React’s model. The sketch should be read as an early learning step, not as a type shape to memorise. The teaching version below keeps the element and Fiber separate:
interface DidactElement {
type: unknown;
key: string | null;
props: Readonly<Record<string, unknown>>;
}
// Deliberately unlike the sketch, this does not extend DidactElement.
interface TeachingFiberRecord {
identity: unknown;
pendingInputs: unknown;
return: TeachingFiberRecord | null;
child: TeachingFiberRecord | null;
sibling: TeachingFiberRecord | null;
hostInstance: unknown;
alternate: TeachingFiberRecord | null;
commitMetadata: number;
schedulingMetadata: number;
}The word teaching matters. This is still a smaller, friendlier model. React’s real Fiber type does not extend ReactElement.
Years of work on React have made the real Fiber record much larger than this teaching version. In React 19.2.0, it includes details for tree navigation, component state, pending updates, scheduling and the final commit. A selected TypeScript-shaped translation of the pinned React v19.2.0 internal Flow type looks closer to this:
interface FiberNode {
// Identity and kind
tag: WorkTag;
key: string | null;
elementType: unknown;
type: unknown;
stateNode: unknown;
// Tree topology
return: FiberNode | null;
child: FiberNode | null;
sibling: FiberNode | null;
index: number;
// Ref work
ref: unknown;
refCleanup: (() => void) | null;
// Inputs and calculated state
pendingProps: unknown;
memoizedProps: unknown;
updateQueue: unknown;
memoizedState: unknown;
dependencies: unknown;
mode: TypeOfMode;
// Commit metadata
flags: Flags;
subtreeFlags: Flags;
deletions: FiberNode[] | null;
// Scheduling summaries
lanes: Lanes;
childLanes: Lanes;
// The other side of the double buffer
alternate: FiberNode | null;
}The field names reveal the architecture.
tag, elementType and type overlap, but they are not interchangeable. tag tells the reconciler what category of work this is, such as a function component or host component. elementType preserves the element identity used during reconciliation. type is the resolved component or host type React will process. Wrappers such as memoisation can make the last two differ.
stateNode is tag-dependent. A HostComponent Fiber points to a host instance, such as a DOM element in React DOM. A HostRoot Fiber points to its FiberRoot, whose containerInfo identifies the host container. A class component Fiber points to its class instance; a function component Fiber normally has null. Calling it simply “the DOM node” hides too much.
pendingProps are the inputs for the render being attempted. memoizedProps are the inputs from the work that was last completed. memoizedState records state used by completed work. On a function component it is the head of the Hook list. updateQueue is tag-dependent and carries queued updates, callbacks or effect data; it is not the “before” half of a paired field.
The paired summary fields are particularly important:
flagsdescribes work on this Fiber;subtreeFlagssummarises relevant work below it.lanesdescribes pending work on this Fiber;childLanessummarises pending work below it.
Together, these summaries tell React whether a branch contains pending work or changes that need to be committed. If a branch has nothing relevant, React can skip it instead of checking every Fiber underneath it.
Read diagram transcript
- A React element is immutable input. It describes a Profile with a key but does not track the work needed to reconcile it.
- React creates or reuses a FiberNode. The Fiber stores identity, topology, state, effects, lanes and its alternate.
- Commit publishes completed work to host output such as a DOM section.
- In the linked Fiber tree, App points to Header through child. Header points to Main through sibling. Header and Main point back to App through return.
The navigation fields are the part I keep returning to. child moves down, sibling moves sideways, and return moves back toward the parent. Together they preserve the next possible directions of traversal in the tree itself.
JavaScript uses stack frames to keep track of active function calls, including where execution should return when each call finishes. React cannot pause and later resume that native call stack on its own. A Fiber gives React a separate, persistent record for one position in the component tree, including links to its child, sibling and parent. That is why Fiber is sometimes compared to a virtual stack frame: it lets React remember where its own traversal should continue without replacing the JavaScript stack.
3. Stage 01: Initial Render Phase
Let’s start from the beginning of a React application:
const root = createRoot(document.getElementById("root"));
root.render(<App />);createRoot() creates the top-level control structure React uses to manage this rendered application. Internally, there are two related concepts that are easy to blur together when first reading React’s source: the root object itself, usually discussed as a FiberRoot, and the HostRoot Fiber that sits at the top of the Fiber tree.
For this stage, the useful mental picture is:
DOM container
↓
FiberRoot
↓
HostRoot Fiber
↓
App
The two root structures have different roles:
-
FiberRoot: This is the control structure for the rendered tree. Among other things, it tracks which Fiber tree is currently committed and later becomes the place where React summarises pending scheduled work. -
HostRootFiber: This is the first actual Fiber in the tree and the entry point into the component tree.
That distinction becomes more useful later. For now, calling root.render(<App />) gives React its first element description and schedules the work required to turn that description into a rendered tree.
Turning returned elements into Fibers
Suppose App returns:
function App() {
return (
<main>
<Header />
<Dashboard />
</main>
);
}
On the initial render, React handles this in three broad steps:
-
Evaluate
App. React processes theAppFiber, runs the component and receives more React elements. -
Read the desired children. Those returned elements describe what should appear below
App. -
Create the child Fibers. There are no existing child Fibers from a previously committed version to preserve, so React creates the Fiber records needed to represent those elements.
Conceptually:
HostRoot
↓
App
↓
main
↓
Header → Dashboard
This is the first time reconciliation appears in our story. On an initial mount, React is not yet comparing those children with an existing version of the same subtree. It is establishing the Fiber structure for the returned elements. In the source, reconcileChildren chooses a mount path when there is no existing current counterpart and uses mountChildFibers to create those child Fibers.
The important part is not the function name. The important part is that JSX descriptions are becoming persistent work records.
How React moves through the initial tree
This is where Fiber’s explicit traversal starts paying off. One of the central functions in the render work loop is:
performUnitOfWork()
The name is wonderfully literal. React takes one Fiber and performs that unit of render work.
Inside that process, the traversal follows three broad steps:
-
Begin work on the current Fiber.
beginWork()handles the downward side of traversal. Depending on the Fiber’stag, it may evaluate a component, process updates, read context, reconcile the children returned by that component, or decide that a branch can be skipped. -
Move down when child work exists. If
beginWork()produces a child that requires processing, React moves into that Fiber next. -
Complete the branch when no child work remains. React travels back through the branch and performs the completion side of the traversal.
A simplified event trace might look like:
begin App
begin Dashboard
begin Chart
complete Chart
begin Feed
complete Feed
complete Dashboard
complete App
Read diagram transcript
- beginWork descends depth first in this order: Root, A, C. C has no child, so React completes C.
- React continues to sibling D, begins and completes it, then completes parent A.
- The walk moves to sibling B, begins and completes child E, completes B, then finally completes Root.
- The complete pass prepares host work for that branch and bubbles subtree flags toward the root.
Notice how close this still looks to the recursive traversal we started with. The crucial difference is not that React discovered a completely different way to walk a tree.
The difference is that the location of that traversal now exists in persistent Fiber records that React controls.
Under the hood: one unit of render work
With large amounts of error handling, profiling and special cases removed, the control flow is conceptually similar to:
function performUnitOfWork(unitOfWork: FiberNode): void {
const current = unitOfWork.alternate;
const next = beginWork(current, unitOfWork, renderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}There are already two unfamiliar names in that real-source-shaped example: alternate and renderLanes. We do not need either of them yet.
On the first pass through the architecture, the line to watch is simply:
workInProgress = next
React keeps an explicit pointer to the Fiber it intends to process next. That is the piece the old recursive mental model could not expose so easily.
Now the application exists. The more interesting question is what happens when somebody changes it.
4. Stage 02: Update Phase
Consider one of the most ordinary pieces of React code possible:
function Search() {
const [query, setQuery] = useState("");
return (
<input value={query} onChange={(event) => setQuery(event.target.value)} />
);
}
The user types the letter r. At application level, we know what happens: onChange runs and setQuery('r') requests an update. But Fiber makes the next question much more interesting:
This is where the persistent Fiber tree matters. React is not rediscovering the application from scratch. The Search component already has a Fiber record. That Fiber participates in a tree whose parent path eventually leads back to the HostRoot, and from there to the FiberRoot we created during Stage 01. React can attach work to that existing structure.
The update is stored before the tree is re-rendered
For stateful Fibers, updateQueue is one part of the bookkeeping React uses to preserve pending updates. The exact structure depends on the kind of Fiber:
-
Function components: Hook state keeps its own queue structures.
-
Class components and roots: They use different update-queue shapes.
The shared architectural idea matters more than treating every queue as identical:
An update is recorded first. Rendering that update is a separate operation.
That separation gives the scheduler an opportunity to decide when the work should actually happen. For a function-component state update, the real source contains a path involving functions such as:
dispatchSetState()
↓
enqueueConcurrentHookUpdate()
↓
scheduleUpdateOnFiber()
↓
ensureRootIsScheduled()
There is more detail around that path, but the direction is the important part. The update begins at one Fiber and ends up making the root aware that something inside its tree requires work.
Where does the root come back into the story?
This is why introducing FiberRoot during Stage 01 is useful. The root is not just an object created because createRoot() needed somewhere to return from. It is React’s scheduling boundary for the tree. When an update occurs inside Search, React can walk the relevant bookkeeping back toward that root and ensure the root will be considered for rendering.
In other words:
Search update
↓
Search Fiber
↓
ancestor Fiber bookkeeping
↓
HostRoot
↓
FiberRoot has pending work
The root does not yet tell us exactly when this update will run relative to every other possible update. That belongs to Stage 05, where scheduling and priority become the main subject. For now, it is enough to understand that an interaction has been converted into pending React work.
Under the hood: where lanes first appear
There is one scheduling detail worth seeing here because it appears directly in the update path. When React processes a state update, requestUpdateLane(fiber) assigns that update to a lane.
For now, I would define a lane only this far:
A lane is part of React’s internal representation of scheduled work.
Nothing more is required yet. The important flow is:
setState()
↓
update created
↓
update receives scheduling information
↓
work is marked through the Fiber tree
↓
FiberRoot knows something is pending
We will come back to lanes in Stage 05, where they actually become useful rather than another field name to remember. At this point React knows that work exists. It still needs to calculate what the next version of the interface should be. That is the render and reconciliation phase.
5. Stage 03: Render and Reconciliation Phase
The initial render had a convenient property: there was no previous version of the application to preserve. Updates are different. The screen already represents a successfully committed UI, and React cannot safely destroy that representation while it is still deciding what the next one should become.
This is where another Fiber field finally earns its place in the story:
alternate
Current and work in progress
React uses three related ideas during an update:
-
Current: The committed Fiber tree that produced the interface currently on screen.
-
Work in progress: The candidate version React prepares during the next render.
-
alternate: The link between corresponding Fiber records on those two sides, when both records exist.
Read diagram transcript
- root.current points at tree A, the state associated with the currently committed interface.
- React prepares tree B as work in progress. Corresponding Root, Nav and Main Fibers in the two trees point to each other through alternate.
- Once render finishes, commit first applies flagged host mutations from the finished tree.
- React then assigns finishedWork to root.current. Layout effects run after that publication and passive effects follow later.
It is common to describe this as “two trees”, which is a useful mental model as long as we do not imagine React eagerly maintaining two completely independent copies of every Fiber forever. The alternate records are allocated lazily and then reused. Across commits, the records can exchange roles.
A Fiber that represented work in progress for this render may become current after commit. On a later render, the previous record can become the work-in-progress side again. The React source describes this as a double-buffering technique. The benefit is important:
React can prepare another version of the interface without partially replacing the one the user is currently looking at.
If rendering pauses, restarts or is abandoned, the current tree remains the committed truth. The unfinished candidate never becomes visible.
Reconciliation: can the existing Fiber still represent this element?
Suppose a component previously returned:
<ul>
<Result key="a" />
<Result key="b" />
</ul>
and now returns:
<ul>
<Result key="a" />
<Result key="c" />
</ul>
React has existing child Fibers from the current tree and a new set of element descriptions from this render. Reconciliation connects the two.
For each relevant child position, React is effectively asking:
Can the Fiber I already have continue representing this returned element, or has the identity at this position changed?
Element type and key are central to that decision:
-
Matching identity: When the type and key identify the same child, React can preserve the existing Fiber’s relationship to the tree and create or reuse its work-in-progress counterpart with the new inputs.
-
Changed identity: When the returned element represents a different child, React needs a Fiber for the new element and records that the old one should not remain in the committed result.
Read diagram transcript
- A component returns React elements. reconcileChildren receives that new description and checks whether the Fiber being processed has a current counterpart.
- When current is null, React calls mountChildFibers and creates the new child Fibers without separate Placement flags because placement of their new ancestor covers the subtree.
- On update React calls reconcileChildFibers. The example compares current keys A, B and C with returned keys B, D and A.
- Matching Fibers B and A can be reused. New Fiber D receives Placement for insertion, and A receives Placement when it must move after B. Removed Fiber C is stored in the parent deletions array and the parent receives ChildDeletion.
This is why keys are much more interesting than “the thing React complains about when I map an array”. Keys participate in preserving identity across one child list and the next.
Returning to the work loop
Once React has chosen the root and the work relevant to this render, the Fiber work loop begins walking the work-in-progress tree. The same performUnitOfWork() idea we saw during initial render is still at the centre. Each unit follows the same basic sequence:
-
Begin the Fiber.
beginWork()can process update queues, evaluate components, read context, handle Suspense paths, reconcile returned children and decide whether a Fiber or subtree can be skipped. -
Descend when there is a child. If
beginWork()returns a child, that child becomes the next unit of work. -
Complete the branch when there is no child. If
beginWork()returnsnull, React has reached the end of that downward path, socompleteUnitOfWork()starts working back through the tree.
Conceptually, with:
App
└── Dashboard
├── Chart
└── Feed
├── ItemA
└── ItemB
the event order looks roughly like:
begin App
begin Dashboard
begin Chart
complete Chart
begin Feed
begin ItemA
complete ItemA
begin ItemB
complete ItemB
complete Feed
complete Dashboard
complete App
The traversal is depth first, but because the position lives in the Fiber structure and a workInProgress pointer, React has explicit control over the boundary between those units. Those explicit boundaries are what later let scheduling policy decide whether React should continue.
Bailouts: sometimes the best work is work React can avoid
Fiber’s persistent bookkeeping lets React decide how much of a branch needs attention:
-
Check the current Fiber: If its props, relevant state and context have not changed, and it has no scheduled work for this render, React may avoid evaluating the component again.
-
Check the subtree: Subtree summaries tell React whether a descendant still contains relevant work.
-
Choose the useful path: If no matching work exists below, React can skip the branch. If work exists deeper in the tree, React can preserve the parent and continue into the children that matter.
This is an important part of the architecture: Fiber does not only give React places where it could pause. It also carries enough information to help React decide which parts of a tree are worth visiting at all.
So when does React remember what must change in the DOM?
By the end of render, React is not supposed to have partially published this candidate tree to the currently committed DOM. It needs a way to remember what observable work the finished tree will require later. This is the right moment to introduce flags.
React records commit work in two related places:
-
flags: Describe commit work associated with the Fiber itself. Common examples includePlacement,Update,ChildDeletionandRef. -
subtreeFlags: Summarise whether relevant commit work exists somewhere below the Fiber.
Both fields are metadata produced while React calculates the result.
The Fiber tree is not only saying:
Here is the next shape of the application.
It is also carrying information closer to:
When this version is published,
this node needs inserting,
this one needs updating,
and that old child needs removing.
That is all we need to know about flags for now. We introduced them at the end of render because the next stage uses them immediately.
6. Stage 04: Commit Phase
Render calculates a possible future. Commit publishes one prepared result.
The flags we just introduced provide part of that bridge. During rendering, a Fiber may have been marked with Placement, Update, ChildDeletion, Ref or other effect-related work. During commit, React can use those flags and their subtree summaries to locate the parts of the prepared tree that require observable changes. It does not need to rediscover every decision from scratch.
What happens to the current tree?
Remember that the FiberRoot points at the currently committed Fiber tree through its current pointer. During rendering, React has prepared finishedWork. A simplified portion of the commit ordering looks like this:
commitBeforeMutationEffects(root, finishedWork, lanes);
commitMutationEffects(root, finishedWork, lanes);
root.current = finishedWork;
commitLayoutEffects(finishedWork, root, lanes);One subtle point in that order is worth noticing:
root.current = finishedWork
The assignment does not itself mean “update the DOM”. The mutation work has already used the prepared flags to perform the relevant host changes. React then changes the root’s current pointer so the finished Fiber tree becomes the committed tree before layout effects run against that result. The work-in-progress version has effectively become current. On a future update, the alternate records can be reused in the opposite role.
Commit itself has several meaningful moments
The commit is easiest to follow as an ordered sequence:
-
Before mutation: React handles work that must happen immediately before host changes, such as capturing certain class-component snapshots.
-
Mutation: React inserts, removes and updates host nodes. It also handles deletion-related work and detaches refs where required.
-
Make the finished tree current: React sets
root.currenttofinishedWork, so layout work sees the newly committed Fiber tree. -
Layout: React runs layout effects, relevant class lifecycles and ref attachment against the committed result.
-
Passive effects later: React handles passive effects separately after the main commit work.
This internal separation makes public APIs such as useLayoutEffect and useEffect easier to reason about. They are not arbitrary lifecycle choices. They correspond to different moments around publication of the finished tree.
Why can render be interruptible while commit cannot?
This is where current versus work in progress becomes much more than an implementation curiosity. React can prepare a candidate result without exposing it, so the user keeps seeing the previously committed tree if React:
-
Pauses the render: The candidate can wait and continue later.
-
Restarts the render: React can prepare a replacement candidate while the current tree remains visible.
-
Abandons the render: An obsolete candidate can be discarded before it becomes visible.
Once React begins publishing a prepared result, however, leaving the DOM halfway between two valid states would be dangerous. For ordinary React DOM commits, the mutation and layout portion is therefore synchronous and non-interruptible. Concurrency applies to preparing the result. It does not mean React deliberately exposes half a commit.
At this point we understand the full path from elements to Fibers, from an update to a work-in-progress tree, and from render metadata to a committed DOM. One question remains: the original motivation for Fiber was not merely to reorganise React’s internal objects. It was to gain control over when that work happens.
7. What Fiber made possible
Return to the problem from section 1. A user is typing into a search field while React also has a large results panel to update. Both pieces of work matter, but they do not necessarily deserve the same urgency.
Fiber gives React the architecture to represent rendering as explicit units, but something still needs to decide which pending work should be rendered and whether an in-progress render can yield. That is the job of React’s scheduling machinery.
Now lanes become useful
We briefly encountered a lane when setState() created an update during Stage 02. Now we finally need to understand what it was doing there. Modern React represents sets of pending work using lanes.
In the React implementation audited for this article, Lanes is represented as a bitmask. Different bits and groups correspond to different scheduling categories, including synchronous work, input-continuous work, default work, transitions, retries, hydration-related work, idle work, offscreen work and other internal categories.
The exact bit assignments are not particularly useful to memorise. Thinking in sets is much more helpful because React is really trying to answer questions such as which work is pending, which work is suspended, what has become available to retry, whether some work has waited long enough to expire, and which pieces of work need to travel together.
The Fiber tree carries lane information close to where updates originate. The FiberRoot carries aggregate scheduling state for the whole rendered tree. This is why it is more accurate to say that React schedules roots and lane sets, not that it maintains one global priority queue containing individual Fiber nodes.
Read diagram transcript
- The root tracks pending work as lanes. The diagram groups conceptual priorities as Sync, Input, Default, Transition and Idle.
- React selects the next lanes for a render, then processes Fiber A and Fiber B as separate units.
- Between units, concurrent rendering can check shouldYield. It may let the browser handle input or paint before resuming at Fiber C.
- If urgent input arrives while transition work is in progress, React can choose the higher-priority lane and later resume or restart the transition. This is cooperative scheduling, not a fixed 16 millisecond timer.
Once React selects a root and the lanes it intends to render, reconciliation still largely proceeds through that tree using the depth-first Fiber traversal we already followed. Scheduling chooses the work, and Fiber performs it.
How can React actually pause?
The architecture becomes clearest when comparing the two work-loop policies. A synchronous work loop can conceptually continue until no Fiber remains:
function workLoopSync(): void {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrentByScheduler(): void {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}The interesting difference is not performUnitOfWork(). It is the policy around it.
The synchronous loop continues until the tree work is complete. The concurrent loop can stop when shouldYield() says the renderer should give control back. Because React holds the next position in workInProgress, the fact that the JavaScript stack unwinds does not necessarily mean React has forgotten where the traversal was. The cross-component progress exists in Fiber state. That is the architectural capability the old reconciler lacked.
React can yield between Fibers, not inside arbitrary JavaScript
This limitation is important enough to repeat. Suppose a component executes:
function ExpensiveComponent() {
performOneHugeSynchronousCalculation();
return <Result />;
}
Fiber cannot freeze the function in the middle of performOneHugeSynchronousCalculation() and later continue on the next JavaScript instruction. That component still runs on the normal JavaScript call stack until it returns. React can make a scheduling decision between Fiber units of work. Those boundaries give React places where it can pause the tree walk and continue later. They do not let React stop ordinary JavaScript in the middle of running.
What happened to the famous 16 millisecond Fiber explanation?
My original notes, like many early Fiber articles, focused heavily on the roughly 16.7 millisecond frame interval and browser APIs such as requestIdleCallback and requestAnimationFrame. That remains useful historical intuition because it explains the browser problem React was trying to respect. It should not be mistaken for the modern implementation. React is not simply assigning every Fiber a 16 millisecond timer and performing the rest through requestIdleCallback.
The durable architectural idea is this:
Long uninterrupted JavaScript competes with the browser, so React needed rendering work that could be divided into explicit units and scheduled with more control.
The scheduler evolved, but that requirement did not.
What if more urgent work arrives?
Suppose React is preparing a transition that rebuilds a large results panel. Partway through that render, the user interacts with something more urgent. Fiber and the scheduler give React room to reconsider what it is doing.
Depending on the circumstances, React may continue work after yielding, restart a render with a different selected set of lanes, or abandon background work that has become obsolete. That last capability matters because sometimes the best optimisation is not completing work faster. It is recognising that the work no longer matters.
Fiber arrived in React 16, but many reasons for the rewrite became visible only over the releases that followed.
React 16 initially used the new architecture synchronously. Its release notes explicitly said the planned async rendering features were not enabled yet. That detail matters because it separates the foundation from the capabilities later built on top of it.
By React 18, concurrent rendering exposed the idea more clearly: React can begin a render, pause it, continue later or abandon it while preserving a consistent committed interface.
Fiber made room for that progression in four steps:
- Persistent Fiber records let React remember its position in the tree and control where the traversal continued.
- Lanes and root scheduling gave pending work different scheduling characteristics.
- Current and work-in-progress records let React prepare without immediately publishing.
- React recorded the required changes as flags, then applied them during a separate commit so the user saw only the completed result.
On top of those pieces, React could build prioritised and interruptible rendering, transitions, Suspense-related behaviour, background preparation, and the ability to discard obsolete renders.
That does not mean “Fiber equals concurrency”. Fiber is lower level. It is the architectural substrate that made concurrency a practical rendering mechanism rather than an API-shaped promise.
And this is why I find Fiber more interesting than most React API discussions.
Hooks can change. Recommended patterns can change. Framework fashions definitely change.
But Fiber answers a deeper engineering question:
What architecture would a UI library need if rendering could no longer be treated as one synchronous function call?
React’s answer was to stop surrendering the cross-component traversal entirely to an unbroken call stack. It built a persistent, stack-like structure it could navigate, summarise, schedule and reuse.
8. A short recap
If you only remember eight things from this article, I would keep these:
- React elements are descriptions. They tell React what UI you want.
- Fibers are persistent work records and unit boundaries. They hold what React needs to reconcile one position across time.
- Fiber replaced React’s old stack reconciler architecture. The old recursive approach made interruption and reprioritisation difficult.
child,siblingandreturnmake the traversal explicit. The structure remembers where React can move next.- React schedules roots and lane sets, not a global queue of individual Fibers. Once selected, work proceeds through the tree.
- Current and work-in-progress records isolate preparation from the visible UI. Their roles alternate across commits.
- Render and commit are different jobs. Render evaluates and prepares; flags drive a synchronous publication step.
- Fiber changed what React could become. It created the foundation for rendering that can be prioritised, continued, restarted or abandoned.
When I originally started digging into Fiber, I expected to find a cleverer virtual DOM algorithm. What I found was much more interesting.
React had moved the state of its reconciliation traversal into an explicit structure it could inspect and control. That structure carried not only where the work should continue, but also what mattered in each subtree, what had changed, which work was pending and which prepared tree could safely become current.
That is the idea that made Fiber finally click for me.
Once you see React’s reconciler as scheduled work over a persistent tree rather than simply “diffing the virtual DOM”, the model becomes easier to follow. A surprising amount of modern React then starts making more sense.
Sources and further reading
- Andrew Clark’s React Fiber Architecture is a historical, unofficial design note that explains Fiber’s original goals and virtual-stack-frame analogy.
- React’s Render and Commit guide explains the public render and commit model.
- The React 16 release notes introduce Fiber and clarify that async rendering was not enabled in that release.
- The React 18 release notes explain interruptible concurrent rendering and consistent commits.
- The technical detail in this draft was audited against the pinned React v19.2.0 source for Fiber fields, child reconciliation, Fiber reuse, root scheduling, lanes, work loops, commit and the Scheduler.