Frontend Development 28 min read

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.

Youzan Coder
Youzan Coder
Youzan Coder
Understanding React Fiber Architecture, Work Units, and Scheduling

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 ~ commit

Therefore, 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.

front-endRenderingJavaScriptReactweb performanceSchedulingFiber
Youzan Coder
Written by

Youzan Coder

Official Youzan tech channel, delivering technical insights and occasional daily updates from the Youzan tech team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.