- Published on
Concurrent Rendering and Lane Prioritization in React 18
Introduction
React Concurrent Rendering optimizes UI performance by breaking down updates into different priority lanes, allowing React to manage tasks based on urgency. The core idea is that updates are categorized into 31 lanes, each representing a different priority level. React uses these lanes to schedule tasks efficiently, ensuring high-priority tasks (like user inputs) are processed immediately, while lower-priority tasks (like background updates) are deferred.
The scheduler interacts with the JavaScript event loop, processing tasks in short bursts (typically 5ms) to keep the UI responsive. React's ability to interrupt lower-priority tasks when higher-priority ones arise ensures that the most critical updates are rendered promptly, maintaining a seamless user experience even during complex rendering tasks.
- Key Steps in React Concurrent Rendering
- React Priority System
- Update Lanes
- Handling Update Priority
- Scheduler
- Real-Life Analogy


Key Steps in React Concurrent Rendering
Triggering an Update: An update can be initiated by various actions, including user interactions, network responses, or timers.
Scheduling the Update: React determines the priority of each update task and adds an update object to the fiber's update queue.
Rendering: React calls the component function and creates or updates the fiber tree based on the changes.
Commit: All changes are committed to the real DOM in a single, synchronous step, followed by the execution of
useLayoutEffect()
anduseEffect()
hooks.
React Priority System
There are three priority systems in React:
Event Priority: marks the priority of user events based on the type of event. Common event priorities include Discrete, Continuous, and Default. For example,
Discrete events (e.g., click, keydown, focus) have the highest priority because they directly affect user input responsiveness.
On the other hand, Continuous events (e.g., mousemove, drag, scroll, mouseover) can be handled more gradually to avoid blocking the browser's main thread.Scheduler Priority: used by React's internal scheduler to prioritize tasks. React defines several scheduler priority levels, such as Immediate, UserBlocking, Normal, Low, and Idle, each indicating how soon a task should be processed.
The scheduler priority level can be found in the following file:
https://github.com/facebook/react/blob/v18.3.1/packages/scheduler/src/forks/Scheduler.js#L324Lane Priority: categorize the fiber's updates into different buckets (or lanes), where each fiber may have multiple updates with different priorities.
When an event triggers an update, React calculates the update lane based on the event priority. The Scheduler uses lane priorities to manage and schedule work. This way, high-priority updates (like those affecting user inputs) can jump ahead of less critical updates (like logging or analytics services updates).
Update Lanes
React defines 31 lanes, represented in binary form, allowing multiple lanes (or priorities) to be represented by a single number. Each bit in a binary number corresponds to a different type of work or priority level, enabling efficient merging or sorting of updates using bitwise operations.
The 31 lanes categorizes into 7 groups, with priority levels arranged from high to low:SyncLanes
> InputContinuousLanes
> DefaultLanes
> TransitionLanes
> RetryLanes
> IdleLanes
> OffScreenLanes
The lanes definition can be found in the following file:
https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberLane.new.js#L34
export const TotalLanes = 31;
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000000100;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000010000;
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111111000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000001000000;
React uses bitwise operations to manipulate lanes and determine the priority of updates efficiently. For instance, to find the highest priority lane, we may perform a bitwise AND operation between lanes and (Not lanes).
var lanes = 0b01011000;
~lanes; // 0b10101000
lanes & ~lanes ; // 0b00001000
To merge lanes, we may perform bitwise OR operation, for example:
var pendingLanes = 0b0101;
var updateLane = 0b0011;
pendingLanes |= updateLane; // 0b0111
Handling Update Priority
enqueueConcurrentHookUpdate()
to mark the fiber for updating. Finally, scheduleUpdateOnFiber()
schedules the update based on the lane priority. The call graph is illustrated as below: 
1. Evaluating Current Update Lane
When setState()
is invoked, React calls requestUpdateLane()
to determine the priority lane for the update.
Transition Task: If the update is part of a transition task (like
Suspense
,useTransition
, oruseDeferredValue
), React searches for an available lane among the 16 transition lanes. These lanes are cycled through, so once lane 16 is reached, the next transition update will loop back to lane 1.Event-Triggered Updates: If the update is not part of a transition task, React invokes
getCurrentUpdatePriority()
to determine the priority of the current task, which is based on the event that triggered the update. This ensures that event-driven updates are assigned the correct level of urgency.Default Handling for Non-Event Updates: In cases where the update is triggered by an I/O operation or other non-event-driven source, React assigns the update to the
DefaultLane
. This provides a fallback mechanism for updates that don't have an associated event or transition context, ensuring they are still processed appropriately.
Considering the example below, the first three setState()
calls, which are invoked directly inside the click handler, are assigned to the synchronous lane (blocking lane). Therefore, like the initial mount, concurrent mode is not activated. However, the fourth setState()
call, which is wrapped inside the startTransition()
method, is assigned to a concurrent lane (TransitionLane1
). As a result, it will be processed in concurrent mode.
export function App() {
return (
<A />
);
}
export function Counter() {
const [count1, setCount1] = useState(10);
const [count2, setCount2] = useState(100);
const [count3, setCount3] = useState(1000);
const [isPending, startTransition] = useTransition();
const handleClick = () => {
setCount1((prev) => {
return prev + 1;
});
setCount1((prev) => {
return prev + 2;
});
setCount2((prev) => {
return prev + 200;
});
startTransition(() => {
setCount3((prev) => {
return prev + 3000;
});
});
};
return (<>
<h2> Counter1: {count1}</h2>
<h2> Counter2: {count2}</h2>
<h2> Counter3: {count3}</h2>
<button onClick={handleClick}>Increase</button>
</>);
}
export default function A({children}: {children?: ReactNode}) {
return (
<>
<h2> A</h2>
<B />
<C />
{children}
</>
);
}
export default function B({children}: {children?: ReactNode}) {
return (
<>
<h2> B</h2>
{children}
</>
);
}
export default function C({ children }: { children?: ReactNode }) {
return (
<>
<h2> C</h2>
<Counter />
<E />
{children}
</>
);
}
export default function E({children}: {children?: ReactNode}) {
return (
<>
<h2> E</h2>
<H />
<I />
{children}
</>
);
}
export default function F({children}: {children?: ReactNode}) {
return (
<>
<h2>F</h2>
<G />
{children}
</>
);
}
export default function G({children}: {children?: ReactNode}) {
return (
<>
<h2> G</h2>
{children}
</>
);
}
export default function H({children}: {children?: ReactNode}) {
return (
<>
<h2> H</h2>
{children}
</>
);
}
export default function I({children}: {children?: ReactNode}) {
return (
<>
<h2> I</h2>
{children}
</>
);
}

2. Marking Pending Updates in the Fiber Tree
React creates an update object with the determined lane and attaches it to the fiber node using enqueueConcurrentHookUpdate()
. This function triggers markUpdateLaneFromFiberToRoot()
, which is responsible for marking all affected fibers from the current fiber up through the parent path to the root. This process serves two primary purposes:
Merging Update Lane Priorities: The function merges the update lane priority into the lanes property of the current fiber node. This merge indicates that the fiber has pending work that needs to be processed during the next render.
Propagating Child Fiber Lanes: It also merges the lanes of all child fibers into the childLanes property of the parent fiber. This merge signals that there is pending work within the child fibers that the parent needs to account for.
For example, consider the Counter component described above. When a button in the Counter component is clicked, triggering the setCount() method that invokes setState()
three times directly and one within a startTransition() method. As a result, the update lane of the Counter fiber becomes 0b10000001
(or decimal 129
), which is a combination of a TransitionLane1 (0b1000000
) and a sysLane (0b1
). This combination ensures that both high-priority and transitional updates are correctly prioritized and handled by React.
Then React sets the lanes property of the Counter fiber to 0b10000001
and the childLanes property to 0
. The fibers along the path from the Counter to the root are updated accordingly, with their lanes reflecting the combined priorities of the updates.


3. Batching and Interrupting Updates
React invokes the scheduleUpdateOnFiber()
function to schedule a re-render. Here’s how React manages this process:
a) Updating Root Pending Lane: React invokes markRootUpdated()
to combine all fibers' update lanes into the root's pendingLanes
, and set the eventTime
for the root, marking the time when the current event occurred. After updating the root, React calls ensureRootIsScheduled()
, which orchestrates several key actions below.
b) Preventing Lane Starvation: React invokes markStarvedLanesAsExpired()
to handle lanes that have exceeded their expiration time. Lanes marked as expired are prioritized for synchronous execution in the next task cycle, bypassing time slicing. This function calculates the expiration time for the current update lane, stores it in the root's expirationTimes array, and combines the expired lanes into root.expiredLanes
, ensuring they are processed without delay.
c) Processing the Highest Update Lane:
Then React invokes getHighestPriorityLane()
to determine the most urgent update lane from the root fiber.
Batching Updates
When the newly identified the highest priority update matches the current pending task (root.callbackPriority
), React skips scheduling a new task and returns immediately, thus the current scheduled task will process the new update with others together. This mechanism prevents redundant task scheduling and ensures that updates with same priority will be processed in the same event cycle.
For instance, in the above Counter component example, three setState()
calls within the click handler are batched, resulting in only one task being scheduled. A fourth setState()
call within a startTransition
is assigned a different lane, thus another scheduled task will process it.
Otherwise, it updates root.callbackPriority
with the new highest priority lane.

Interrupting Lower Priority Updates
When the newly determined highest priority update is higher than the current root.callbackPriority
, React cancels the ongoing task by set the task's callback to null
. This prevents lower-priority work from blocking more critical updates. Then React schedules a new task with the new priority lane to processes this more urgent update first, ensuring that React prioritizes the most important work.
After completing this higher-priority update, React continues by scheduling another task for the next highest priority lane, effectively creating a cascading sequence of tasks based on priority. This dynamic adjustment allows React to handle new, higher-priority tasks efficiently as they emerge, maintaining optimal performance.
Scheduler
Sync Task Queue
If the highest priority update lane is SyncLane
, React invokes scheduleSyncCallback()
to add the task to the syncQueue
queue, which is a simple array that holds all high-priority tasks. React then calls scheduleMicrotask()
to enqueue flushSyncCallbacks()
into the JavaScript microtask queue, ensuring that flushSyncCallbacks()
is executed immediately after the JavaScript event loop completes its current work.
When flushSyncCallbacks()
runs, it processes all pending updates in the syncQueue
synchronously, without introducing any time-slicing delays.
For non-synchronous lanes, React invokes scheduleCallback()
to add the task to the React internal task queue. This queue is managed internally by React and is designed to yield control back to the browser if the task execution time exceeds approximately 5ms. React’s priority-based scheduling system ensures efficient handling of updates, processing them lane by lane, and dynamically adjusting to new, higher-priority updates as they arise.

Scheduler Task Queue
React Scheduler internally maintains a task queue, where each task represents a discrete unit of work. The scheduleCallback()
method is the entry point for adding tasks to this queue.
- a). Calculating Expiration Time: When a task is scheduled, React first calculates an expiration time based on the task's priority. This ensures that high-priority tasks are executed promptly, while lower-priority tasks can be deferred if necessary. You can check the relevant React source code here:
https://github.com/facebook/react/blob/v18.3.1/packages/scheduler/src/forks/Scheduler.js#L324
function unstable_scheduleCallback(priorityLevel, callback, options) {
...
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
...
- b). Creating the Task Object: After calculating the expiration time, React creates a task object and adds it to the React internal task queue. This object holds the task's metadata, such as the start time, expiration time, and scheduler priority.
However, it’s crucial to understand that this task object does not hold information about the pending fiber. The rendering process then begins from the root node, progressing lane by lane based on priority.
Splitting Work into Small Chunks
React’s task management involves two distinct task queues that work in tandem:
React's Internal Task Queue: This queue is managed by React itself. It holds tasks related to rendering updates, state changes, and other internal operations.
JavaScript Event Loop Task Queue: Managed by the browser's JavaScript engine, this queue includes tasks such as DOM events, network requests, and timers.
Here’s how React yields control to the browser and resumes processing tasks:
Enqueuing
performWorkUntilDeadline()
into JavaScript Task Queue React invokes therequestHostCallback()
function, which in turn callsschedulePerformWorkUntilDeadline()
. This function schedules the execution ofperformWorkUntilDeadline()
during the next available slot in the JavaScript task queue. React then yields control, allowing the browser to process other tasks, such as layout and painting.Executing and Managing Tasks in the JavaScript Event Loop Once the JavaScript event loop is ready to process the task queue, it picks up
performWorkUntilDeadline()
and begins execution. This function processes tasks from React's internal task queue. After processing a task, React checks if approximately 5 milliseconds have elapsed. If so,schedulePerformWorkUntilDeadline()
is called again to re-enqueue the functionperformWorkUntilDeadline()
, and exit to allowing the browser to take control momentarily. Later, the JavaScript event loop will pick up the callbackperformWorkUntilDeadline()
again and continue the cycle. This process repeats until all React tasks are completed, ensuring the UI remains responsive.
You can explore the relevant React source code for Scheduler.js here.
https://github.com/facebook/react/blob/v18.3.1/packages/scheduler/src/SchedulerFeatureFlags.js#L13
export const frameYieldMs = 5;
The use of the JavaScript event loop and the 5ms yield time is central to React's concurrent rendering. By distributing the workload across multiple iterations of the JavaScript event loop, React effectively splits rendering tasks into small, manageable chunks, preventing the UI from becoming unresponsive during complex updates.

setTimeout(fn, 0) Drawbacks
The schedulePerformWorkUntilDeadline()
is to add performWorkUntilDeadline()
to the JavaScript task queue. Its implementation varies depending on the environment:
Browser Environment: In a browser environment, React uses
MessageChannel
to enqueue callbacks. Before React v16.10, a combination ofrequestAnimationFrame()
andsetTimeout()
was used. However, this method had its drawbacks, particularly becauserequestAnimationFrame()
halts execution when the tab is backgrounded or when performance-saving measures are in place, leading to a poor user experience.
The pull request that introduced this change can be found here.
https://github.com/facebook/react/pull/16214While
setTimeout(fn, 0)
can be a viable solution, but according to the HTML Standard, when the nesting level of setTimeout calls exceeds 5, the timeout is automatically adjusted to 4 milliseconds. This delay, although small, can accumulate and lead to performance issues.
More details on this behavior can be found in the HTML specification here.
https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeoutNode.js Environment: In a Node.js environment, React leverages
setImmediate()
, a built-in function in Node.js, to enqueue the callback function. This function is specifically designed for scheduling tasks in the next iteration of the event loop, making it ideal for React's task management.
However, it's important to note thatsetImmediate()
is not part of the web standards and major browsers like Chrome and Firefox have removed support for it.
You can refer to thesetImmediate()
here.
https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediateFallback Mechanism: In environments where neither
setImmediate()
norMessageChannel
are available, React falls back to usingsetTimeout()
to ensures that the application remains functional.
Executing React scheduled task
performWorkUntilDeadline()
is the callback of the JavaScript task, which manages the execution of the scheduler tasks. When JavaScript event loop picks up performWorkUntilDeadline()
and executes it, performWorkUntilDeadline()
first records the current time as the start time and subsequently invokes workLoop()
.
workLoop()
begins by selecting the highest-priority task from React’s internal task queue and checks the elapsed time since the start time.
If the time spent is within the 5ms window, the callback of the highest-priority React task is executed. Once the callback function completes, the next highest-priority task is selected from the queue, and the cycle repeats.
If the time spent exceeds 5ms and the task hasn’t expired, control is immediately returned to
performWorkUntilDeadline()
.performWorkUntilDeadline()
will then invokeschedulePerformWorkUntilDeadline()
to reschedule itself in the JavaScript event queue, allowing the browser to take control and perform any necessary rendering or other operations.If the task has expired, the React task's callback is executed without delay."

Rendering
performConcurrentWorkOnRoot()
is the callback function of the React fiber update task.
If the task has expired,
performConcurrentWorkOnRoot()
will callrenderRootSync()
to immediately process rendering without delay.If the task is still within its time limit,
renderRootConcurrent()
is called, which in turn triggersworkLoopConcurrent()
to handle the rendering process.workLoopConcurrent()
checks whether 5ms have passed before processing each fiber. If the time limit is exceeded, control returns toperformWorkUntilDeadline()
, which then callsschedulePerformWorkUntilDeadline()
to place itself back into the JavaScript event queue, allowing the browser to take control. This cycle continues until either all pending updates on the Fiber have been processed or the time limit is reached, at which pointrenderRootSync()
is invoked to complete rendering without further delay.

Real-Life Analogy
Imagine a programmer working on an important task, while a group of people continuously ask questions. Some of these questions are urgent, some are repetitive, and others take a lot of time to answer. The programmer can’t handle everything at once.
To help, a product manager steps in to prioritize the questions, placing them in a queue. The programmer sets a timer and periodically checks if more urgent tasks have come up. If so, he pauses his current work to tackle these high-priority tasks first.
In this scenario, the product manager is akin to the React scheduler, and the programmer is like the rendering process. React, however, operates with a much shorter interval—often just 5 milliseconds—ensuring that even under heavy load, the user experience remains seamless.