Understanding React Fiber Architecture, Work Units, and Scheduling
React Fiber rewrites React’s core algorithm by breaking the diff phase into small, interruptible fiber units, scheduling them during browser idle time with requestIdleCallback, using double‑buffered trees and effect lists to pause, resume, and efficiently commit DOM updates, thereby improving UI responsiveness in large applications.
Preface
Fiber is a complete rewrite of React's core algorithm. The Facebook team spent more than two years refactoring the core of React and introduced the Fiber architecture in React 16 and later versions. This dramatically improves the performance of large React projects and sparks curiosity about its implementation. By studying the source code you can discover many fine‑grained details such as task unit splitting, scheduling, double buffering, and node reuse.
1. Why We Need React Fiber
In classic React rendering the whole update (from setState to DOM commit) runs synchronously on the main thread. For large component trees this blocks the thread for a long time, causing UI lag, dropped frames, and unresponsive animations.
React previously relied on PureComponent , shouldComponentUpdate , useMemo , useCallback to let developers manually prune sub‑trees. The root cause is that JSX is too flexible for the engine to infer what can be skipped.
Why does long‑running JavaScript affect interaction and animation? Because JavaScript runs on the browser’s main thread together with style calculation, layout, and painting. If JavaScript occupies the thread for too long, those other tasks are blocked, leading to dropped frames.
Fiber solves this by breaking the render/update work into small, interruptible units and scheduling them during idle time, allowing the browser to respond to user input promptly. The reconciliation phase becomes interruptible, and the CPU can be yielded back to the browser when needed.
Split the render/update process into smaller, interruptible work units.
Execute the work loop when the browser is idle.
Patch the accumulated results onto the real DOM.
2. Work Units
2.1 What Can Be Split, What Cannot
The process is divided into two stages: diff (render/reconciliation) and patch (commit). The diff stage compares the previous and next virtual instances, finds differences, and can be split into smaller chunks. The patch stage applies all DOM changes; although it could be split, doing so may cause state inconsistencies and offers little performance gain.
1.diff ~ render/reconciliation
2.patch ~ commitTherefore, only the diff stage is split; the commit stage is not.
2.2 How to Split
Several naive splitting strategies were considered:
Split by component hierarchy – difficult to estimate work per component.
Split by concrete operations such as getNextState() , shouldUpdate() , updateState() , checkChildren() – too fine‑grained, leading to many tiny tasks.
Neither extreme works well for large components, so a more suitable unit is needed.
2.3 Fiber
The chosen unit is a fiber , which corresponds to a node in the fiber tree (mirroring the virtual DOM tree). Each fiber carries additional bookkeeping information.
// fiber tree node structure
{
// The local state associated with this fiber.
stateNode,
// Singly Linked List Tree Structure.
child,
return,
sibling,
// Effect
effectTag,
// Singly linked list fast path to the next fiber with side‑effects.
nextEffect,
// The first and last fiber with side‑effect within this subtree.
firstEffect,
lastEffect,
...
}The child , sibling , and return pointers form a linked‑list tree, enabling depth‑first traversal. Effect‑related fields ( effectTag , nextEffect , firstEffect , lastEffect ) store the results of the diff phase, allowing React to pause after each fiber and decide whether to continue based on the browser’s idle time.
3. Browser Capabilities
Before discussing Fiber’s scheduling we need to understand the browser’s rendering pipeline.
3.1 Rendering Frame
A browser paints frames at the device’s refresh rate (typically 60 Hz, i.e., 16 ms per frame). Each frame consists of several stages:
Process input events to give the user immediate feedback.
Handle timers and execute callbacks whose time has arrived.
Begin Frame events (resize, scroll, media query changes, etc.).
Execute requestAnimationFrame callbacks.
Layout – compute geometry and style.
Paint – fill each node with visual content.
After these six stages the browser enters an idle period where requestIdleCallback tasks can run.
3.2 requestIdleCallback
This API queues a function to run when the main thread is idle, allowing low‑priority work (such as Fiber work) to execute without blocking high‑priority interactions. The callback receives a deadline object with two properties:
timeRemaining() – how many milliseconds are left in the current frame.
didTimeout – whether the callback has exceeded its timeout.
Two examples illustrate its behavior:
Single‑frame execution
const sleep = (delay) => {
const start = Date.now();
while (Date.now() - start <= delay) {}
};
const taskQueue = [
() => { console.log("task1 start"); sleep(3); console.log("task1 end"); },
() => { console.log("task2 start"); sleep(3); console.log("task2 end"); },
() => { console.log("task3 start"); sleep(3); console.log("task3 end"); }
];
const performUnitWork = () => { taskQueue.shift()(); };
const workloop = (deadline) => {
console.log(`Remaining time: ${deadline.timeRemaining()}`);
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
performUnitWork();
}
if (taskQueue.length > 0) {
window.requestIdleCallback(workloop, { timeout: 1000 });
}
};
requestIdleCallback(workloop, { timeout: 1000 });All three tasks finish within the first frame because the total time (< 16 ms) is available.
Multi‑frame execution
const sleep = (delay) => {
const start = Date.now();
while (Date.now() - start <= delay) {}
};
const taskQueue = [
() => { console.log("task1 start"); sleep(10); console.log("task1 end"); },
() => { console.log("task2 start"); sleep(10); console.log("task2 end"); },
() => { console.log("task3 start"); sleep(10); console.log("task3 end"); }
];
const performUnitWork = () => { taskQueue.shift()(); };
const workloop = (deadline) => {
console.log(`Remaining time: ${deadline.timeRemaining()}`);
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
performUnitWork();
}
if (taskQueue.length > 0) {
window.requestIdleCallback(workloop, { timeout: 1000 });
}
};
requestIdleCallback(workloop, { timeout: 1000 });Task 1 finishes in the first frame, task 2 starts but exceeds the remaining time, and task 3 is deferred to the next frame.
Long‑running work inside requestIdleCallback should be avoided, and DOM mutations should be performed in requestAnimationFrame instead.
Promises are also discouraged inside requestIdleCallback because their micro‑tasks run immediately after the callback, potentially pushing the frame over the 16 ms budget.
4. React Fiber Execution Mechanics
4.1 Task Scheduling
Fiber processes one work unit at a time. The loop continues while there is work and the renderer has not yielded:
// Flush asynchronous work until there's a higher priority event
while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}shouldYieldToRenderer checks whether the allotted time slice is exhausted. If not, the loop proceeds; otherwise control returns to the browser and the next idle callback resumes work.
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}The scheduling diagram (omitted) shows how Fiber interacts with the browser’s idle periods.
4.2 Traversal Process
Fiber builds a new work‑in‑progress (WIP) tree by depth‑first traversal of the current tree:
Start at the root.
If a node has children, traverse them first.
If no children, look for a sibling; if found, traverse the sibling and merge its effects upward.
If no sibling, ascend to the parent and look for the parent’s sibling.
Repeat until the entire tree is visited.
This is essentially a depth‑first search.
function performUnitOfWork(fiber: Fiber, topWork: Fiber) {
next = beginWork(current, workInProgress, nextRenderExpirationTime);
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
return next;
}The next unit is determined by beginWork based on the current fiber’s type.
switch (workInProgress.tag) {
case HostComponent: {
return updateHostComponent(current, workInProgress, renderExpirationTime);
}
case ClassComponent: {
return updateClassComponent(current, workInProgress, Component, resolvedProps, renderExpirationTime);
}
case FunctionComponent: {
return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderExpirationTime);
}
// ...
}A simplified next‑node selection algorithm:
// If there is a child, process it first
if (fiber.child) {
return fiber.child;
}
// No child – climb up to find a sibling
let temp = fiber;
while (temp) {
completeWork(temp);
if (temp === topWork) {
break;
}
if (temp.sibling) {
return temp.sibling;
}
temp = temp.return;
}4.3 Reconciliation
During the diff phase React compares the previous and next virtual trees, marking nodes with effect tags ( UPDATE , PLACEMENT , DELETION ). The process (simplified) is:
If a node does not need an update, clone its children and skip.
Update the node’s props, state, context.
Run shouldComponentUpdate ; if false, skip.
Call render() to obtain new children and create fibers for them (reusing existing fibers when possible).
If no child fiber is created, finish the unit and move to the sibling; otherwise, descend into the child.
If the time slice expires, pause and resume later via requestIdleCallback .
When the root’s work is done, the tree enters the pending‑commit phase.
4.4 Interruption and Resumption
If the time slice ends, Fiber records the current progress ( firstEffect , lastEffect ) and yields. The next idle callback resumes from the saved point.
4.5 Double Buffering
React maintains two trees: the current tree and a work‑in‑progress tree. After the WIP tree is built, it becomes the new current tree, and the old tree is kept as the alternate for possible reuse.
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// Reset effect list for reuse
workInProgress.effectTag = NoEffect;
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
}This technique limits memory usage to at most two versions of the tree and enables efficient node reuse.
4.6 Effect Collection and Commit
Each fiber that produces side effects links itself into an effect list during completeWork :
function completeWork(fiber) {
const parent = fiber.return;
if (parent == null || fiber === topWork) {
pendingCommit = fiber;
return;
}
if (fiber.effectTag != null) {
if (parent.nextEffect) {
parent.nextEffect.nextEffect = fiber;
} else {
parent.nextEffect = fiber;
}
} else if (fiber.nextEffect) {
parent.nextEffect = fiber.nextEffect;
}
}When reconciliation finishes, the root’s effect list contains all DOM changes. The commit phase walks this list and applies each effect:
function commitAllWork(fiber) {
let next = fiber;
while (next) {
if (fiber.effectTag) {
commitWork(fiber); // actual DOM mutation (omitted for brevity)
}
next = fiber.nextEffect;
}
// Cleanup
pendingCommit = nextUnitOfWork = topWork = null;
}Conclusion
React Fiber transforms the recursive reconciler into an interruptible, loop‑based algorithm. It does not reduce the total amount of work; it merely postpones low‑priority work to idle periods, improving perceived responsiveness. Real performance gains still depend on writing efficient components and avoiding unnecessary renders.
Youzan Coder
Official Youzan tech channel, delivering technical insights and occasional daily updates from the Youzan tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.