How root render schedule works
In how root.render() works
section, the last thing we did is to
schedule via microtask our application's render:
// React code was like this
scheduleImmediateTask(processRootScheduleInMicrotask);
// which will do something similar to this (in almost all cases)
queueMicrotask(processRootScheduleInMicrotask);
To be more precise, we have a scheduled root with pending
work stored in
the global firstScheduledRoot
variable.
The work-loop is the process of rendering your components and displaying them
on the screen. The loop is triggerred by several actions: root.render()
,
components updates, recovering from suspense... etc.
Since we are coming from root.render()
, we will first explain how we
would reach the work loop, and then dive into it.
How processRootScheduleInMicrotask
works
When the callstack becomes empty, the javascript event-loop will process the task queue, and will eventually call the callback scheduled here.
If you are wondering, how can they be multiple roots? It is because React can run in the server too and render for several requests in parallel, or you can do it manually in client.
React code smartly and dangerously plays with globals in order to unlock many of the concurrent features. Most of the time, these global variables represents a compact internal state of a module, and external manipulation is offered via dedicated functions.
processRootScheduleInMicrotask
will loop over the scheduled roots (in our simple case, the render of a small
application via root.render()
, there will only one root). And for each root:
let root = firstScheduledRoot;
while (root !== null) {
// perform logic for current root
root = next;
}
In summary, here is a simplified version of what happened:
// simplified, a lot
const currentTime = Date.now();
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;
// 1
entangleSyncLaneIfInsidePopStateEvent(root);
// 2
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
// 3
if (nextLanes === NoLane) { // no pending work to do
detachRootFromScheduledRoots(root);
} else {
// 4
if (includesSyncLane(nextLanes)) {
mightHavePendingSyncWork = true;
}
}
root = next;
}
// 5
flushSyncWorkOnAllRoots();
- Entangle the root to a
SyncLane
if we are inside apopstate
event. - Call
scheduleTaskForRootDuringMicrotask
to get thenextLanes
for thisroot
. This function is very important and that's when the work-loop will be triggered, we will see it right after. - If there is no pending work (
nextLanes === NoLane
), then the root is removed from the chain: Remember? We may have multiple roots in a linked list way, when there is no pending work to do on a root, it is detached from that list. - If there is still work to do, keep this root in the list and verify if it contains a sync work.
- Flush sync work
How scheduleTaskForRootDuringMicrotask
works
The second step above did make a call to this function which is defined as follows:
Signature
function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number,
): Lane { /* [Not Native Code] */ }
This function returns the highest priority lane that's scheduled (smallest lane).
Implementation steps
Mark starved lanes as expired
In this process, we will go through the root's
pendingLanes
one by one and either compute an expiration time for it if not already computed, or else, check if it expired and add it toroot.expiredLanes
:// simplified
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes;
let lanes = pendingLanes & ~RetryLanes;
while (lanes > 0) {
// in the following two lines, we will get the index of the highest priority
// lane from the lanes, and then we will shift 1 by that index so we obtain
// the current lane with only one significant bit (power of 2)
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
// the expiration time for this lane
const expirationTime = root.expirationTimes[index];
// this means it is not scheduled yet
if (expirationTime !== NoTimestamp) {
// this means this lane isn't suspended or is pinged
// pinged lanes are the lanes that were suspending the root when
// the suspending promise did resolve.
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
// this is when it is not expired
root.expiredLanes |= lane;
}
lanes &= ~lane;
}Check the codebase via this link.
Compute the next lanes
Next, React will compute the next lanes:
// When coming from root.render(children)
const nextLanes = getNextLanes(root, NoLanes);Exit when there is no pending work or suspended on data
When
nextLanes
are equal toNoLanes
(which means there is nopending
work to do), or the root is suspended on data, or the root has a scheduled pending commit callback (we will see it later) then it will:- Cancel the existing callback if any
- Clean the
root.callbackNode
androot.callbackPriority
properties - return
NoLane
Read more here on the real implementation.
Exit when there is a Sync pending work
Sync work will be flushed by
processRootScheduleInMicrotask
and will be detailed in the next section.So when a
SyncLane
, we do exactly same as the previous step, and return aSyncLane
rather thanNoLane
.Schedule the render on the current root
By here, we have a pending concurrent work to do, then we will compute a priority from the
nextLanes
. Wait! what ?The new
callbackPriority
is implemented now as thehighest priority lane
which will be tested against the existing callback priority. If the priority did not change, React would reuse the same task and priority.Or else, we would need to infer a
priority
from thehighest priority lane
.The highest priority lane is the smallest lane from
nextLanes
.There are until now
4
priorities:export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLne;
export const IdleEventPriority: EventPriority = IdleLane;Hey! Why we are assigning
Lane
s values to them ? Well, priorities are like breakpoints for lanes, which means that we can categorize all lanes into4
groups of priorities.You can look at the real implementation here:
// simplified
const lane = getHighestPriorityLane(nextLanes);
if (DiscreteEventPriority > lane) {
return DiscreteEventPriority;
}
if (ContinuousEventPriority > lane) {
return ContinuousEventPriority;
}
if (DefaultEventPriority > lane) {
return DefaultEventPriority;
}
return IdleEventPriority;Then, we will translate the EventPriority to the SchedulerPriority:
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}This part is implemented here.
At this point, we've reached the very last few things to do: Scheduling the work.
const newCallbackNode = scheduleCallback(
// NormalPriority for simple root.render
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
return newCallbackPriority;We won't talk about
scheduleCallback
here since it is well documented in thehow scheduler works
section.performConcurrentWorkOnRoot
will be detailed in a few.Think of
scheduleCallback
like asetTimeout
for now.
Reminder: After looping over all scheduled roots and re-schedule their render,
processRootScheduleInMicrotask
will finally flush sync work on all roots.
How flush sync work on roots works
This function
will loop over all scheduled roots starting from firstScheduledRoot
, will
exclude any non-legacy roots or those who don't have a SyncLane
flag.
Then, it will call the performSyncWorkOnRoot
which will trigger the sync
work-loop.
We will first start by the concurrent
work loop and then the sync
one.