What are Lanes in React source code?
Previously weāve seen how React schedules tasks by their priorities,
an example task would be performConcurrentWorkOnRoot()
,
which targets the fiber tree as a whole,
not the level of work on a single fiber.
The concurrent mode is cool because React is able to work on different tasks for each fiber by their priorities, this low level priority is implement with the concept of āLaneā. This might sounds jargon, donāt worry, weāll dive into it and there are some examples at the end.
1. Three priority systems
Take a look at ensureRootIsScheduled()
again,
this is one of the entries where scheduling method is called.
js
// We use the highest priority lane to represent the priority of the callback.const newCallbackPriority = getHighestPriorityLane(nextLanes);if (newCallbackPriority === SyncLane) {...} else {let schedulerPriorityLevel;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;}newCallbackNode = scheduleCallback(schedulerPriorityLevel,performConcurrentWorkOnRoot.bind(null, root),);}
js
// We use the highest priority lane to represent the priority of the callback.const newCallbackPriority = getHighestPriorityLane(nextLanes);if (newCallbackPriority === SyncLane) {...} else {let schedulerPriorityLevel;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;}newCallbackNode = scheduleCallback(schedulerPriorityLevel,performConcurrentWorkOnRoot.bind(null, root),);}
Interesting, from above code we see that the scheduler priority is derived by
- getting the highest lane priority -
getHighestPriorityLane()
- if not SyncLane, map lanes to Event Priority, then map to Scheduler Priority.
Thus we have 3 priority systems
- Scheduler priority - used to prioritize tasks in scheduler
- Event priority - to mark priority of user event
- Lane priority - to mark priority of work
Since they are for different purposes, they are separated but have logic of mapping like above, weāll cover event system in future episodes, so we donāt touch much details here.
2. What is āLaneā ?
From my youtube video about setState()
,
we know that a fiber holds a linked list of hooks,
and for state hook, it has a update queue which gets run during update(re-render).
Here is the code where an update is created(source)
const lane = requestUpdateLane(fiber);const update: Update<S, A> = {lane,action,hasEagerState: false,eagerState: null,next: (null: any),};
const lane = requestUpdateLane(fiber);const update: Update<S, A> = {lane,action,hasEagerState: false,eagerState: null,next: (null: any),};
Yep, notice there is field called lane
? Lane
is to mark the priority of
the update, which we can also say mark the priority of a piece of work.
Here are all the lanes in React,
just some numbers which is easier to understand in binary forms. Please try to
find the 1
s.
js
// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-timeline.// If those values are changed that package should be rebuilt and redeployed.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: Lanes = /* */ 0b0000000000000000000000000000100;export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;export const DefaultLane: Lanes = /* */ 0b0000000000000000000000000010000;const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;const TransitionLanes: Lanes = /* */ 0b0000000001111111111111111000000;const TransitionLane1: Lane = /* */ 0b0000000000000000000000001000000;const TransitionLane2: Lane = /* */ 0b0000000000000000000000010000000;const TransitionLane3: Lane = /* */ 0b0000000000000000000000100000000;const TransitionLane4: Lane = /* */ 0b0000000000000000000001000000000;const TransitionLane5: Lane = /* */ 0b0000000000000000000010000000000;const TransitionLane6: Lane = /* */ 0b0000000000000000000100000000000;const TransitionLane7: Lane = /* */ 0b0000000000000000001000000000000;const TransitionLane8: Lane = /* */ 0b0000000000000000010000000000000;const TransitionLane9: Lane = /* */ 0b0000000000000000100000000000000;const TransitionLane10: Lane = /* */ 0b0000000000000001000000000000000;const TransitionLane11: Lane = /* */ 0b0000000000000010000000000000000;const TransitionLane12: Lane = /* */ 0b0000000000000100000000000000000;const TransitionLane13: Lane = /* */ 0b0000000000001000000000000000000;const TransitionLane14: Lane = /* */ 0b0000000000010000000000000000000;const TransitionLane15: Lane = /* */ 0b0000000000100000000000000000000;const TransitionLane16: Lane = /* */ 0b0000000001000000000000000000000;const RetryLanes: Lanes = /* */ 0b0000111110000000000000000000000;const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;const RetryLane5: Lane = /* */ 0b0000100000000000000000000000000;export const SomeRetryLane: Lane = RetryLane1;export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;const NonIdleLanes = /* */ 0b0001111111111111111111111111111;export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;export const IdleLane: Lanes = /* */ 0b0100000000000000000000000000000;export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
js
// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-timeline.// If those values are changed that package should be rebuilt and redeployed.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: Lanes = /* */ 0b0000000000000000000000000000100;export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;export const DefaultLane: Lanes = /* */ 0b0000000000000000000000000010000;const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;const TransitionLanes: Lanes = /* */ 0b0000000001111111111111111000000;const TransitionLane1: Lane = /* */ 0b0000000000000000000000001000000;const TransitionLane2: Lane = /* */ 0b0000000000000000000000010000000;const TransitionLane3: Lane = /* */ 0b0000000000000000000000100000000;const TransitionLane4: Lane = /* */ 0b0000000000000000000001000000000;const TransitionLane5: Lane = /* */ 0b0000000000000000000010000000000;const TransitionLane6: Lane = /* */ 0b0000000000000000000100000000000;const TransitionLane7: Lane = /* */ 0b0000000000000000001000000000000;const TransitionLane8: Lane = /* */ 0b0000000000000000010000000000000;const TransitionLane9: Lane = /* */ 0b0000000000000000100000000000000;const TransitionLane10: Lane = /* */ 0b0000000000000001000000000000000;const TransitionLane11: Lane = /* */ 0b0000000000000010000000000000000;const TransitionLane12: Lane = /* */ 0b0000000000000100000000000000000;const TransitionLane13: Lane = /* */ 0b0000000000001000000000000000000;const TransitionLane14: Lane = /* */ 0b0000000000010000000000000000000;const TransitionLane15: Lane = /* */ 0b0000000000100000000000000000000;const TransitionLane16: Lane = /* */ 0b0000000001000000000000000000000;const RetryLanes: Lanes = /* */ 0b0000111110000000000000000000000;const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;const RetryLane5: Lane = /* */ 0b0000100000000000000000000000000;export const SomeRetryLane: Lane = RetryLane1;export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;const NonIdleLanes = /* */ 0b0001111111111111111111111111111;export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;export const IdleLane: Lanes = /* */ 0b0100000000000000000000000000000;export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
Just like lanes on the road, the rule is to drive on different
lanes based on your speed, meaning the smaller the lane is
the more urgent the work is, the higher priority it is.
So SyncLane
is 1
here.
There are many lanes. We donāt dive into each of them but rather understand how it works generally in this episode.
2.1 Bitwise operation
The lanes are just numbers, there are a lot of bitwise operations in React source code, letās get familiar with that.
Here are some examples that explain themselves.
js
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {return (a & b) !== NoLanes;}export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {return (set & subset) === subset;}export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {return a | b;}export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {return set & ~subset;}export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {return a & b;}
js
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {return (a & b) !== NoLanes;}export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {return (set & subset) === subset;}export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {return a | b;}export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {return set & ~subset;}export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {return a & b;}
2.2 remember childLanes
?
In the episode of
how does React bailout work in reconciliation,
weāve also touched a bit of lanes
and childLanes
of a fiber.
Each fiber knows:
- priorities for the work of itself -
lanes
- priorities for the work of its descendant -
childLanes
.
3. Look at performConcurrentWorkOnRoot()
again
Here is the basic flow of how a work is scheduled and run.
- get the
nextLanes
of the fiber tree - map it to scheduler priority
- schedule the task to reconcile
- reconciliation happens, process the work from root
I guess the magic lies in actually reconciliation, that is where lane info is used.
js
let lanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);...prepareFreshStack(root, lanes);
js
let lanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);...prepareFreshStack(root, lanes);
prepareFreshStack()
means restart the reconciliation, remember there is a cursor(workInProgress
) to track the current fiber, usually React would pause and resume from previous position, but in case of error or some weird cases, we need to give up current jobs done and redo them from beginining, this is what fresh
means.
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {root.finishedWork = null;root.finishedLanes = NoLanes;const timeoutHandle = root.timeoutHandle;if (timeoutHandle !== noTimeout) {// The root previous suspended and scheduled a timeout to commit a fallback// state. Now that we have additional work, cancel the timeout.root.timeoutHandle = noTimeout;// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check abovecancelTimeout(timeoutHandle);}if (workInProgress !== null) {let interruptedWork = workInProgress.return;while (interruptedWork !== null) {unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);interruptedWork = interruptedWork.return;}}workInProgressRoot = root;workInProgress = createWorkInProgress(root.current, null);workInProgressRootRenderLanes =subtreeRenderLanes =workInProgressRootIncludedLanes =lanes;workInProgressRootExitStatus = RootIncomplete;workInProgressRootFatalError = null;workInProgressRootSkippedLanes = NoLanes;workInProgressRootInterleavedUpdatedLanes = NoLanes;workInProgressRootRenderPhaseUpdatedLanes = NoLanes;workInProgressRootPingedLanes = NoLanes;enqueueInterleavedUpdates();}
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {root.finishedWork = null;root.finishedLanes = NoLanes;const timeoutHandle = root.timeoutHandle;if (timeoutHandle !== noTimeout) {// The root previous suspended and scheduled a timeout to commit a fallback// state. Now that we have additional work, cancel the timeout.root.timeoutHandle = noTimeout;// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check abovecancelTimeout(timeoutHandle);}if (workInProgress !== null) {let interruptedWork = workInProgress.return;while (interruptedWork !== null) {unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);interruptedWork = interruptedWork.return;}}workInProgressRoot = root;workInProgress = createWorkInProgress(root.current, null);workInProgressRootRenderLanes =subtreeRenderLanes =workInProgressRootIncludedLanes =lanes;workInProgressRootExitStatus = RootIncomplete;workInProgressRootFatalError = null;workInProgressRootSkippedLanes = NoLanes;workInProgressRootInterleavedUpdatedLanes = NoLanes;workInProgressRootRenderPhaseUpdatedLanes = NoLanes;workInProgressRootPingedLanes = NoLanes;enqueueInterleavedUpdates();}
We can see that in prepareFreshStack()
some variables are just reset.
There are quite a few of them about lanes.
workInProgressRootRenderLanes
subtreeRenderLanes
workInProgressRootIncludedLanes
workInProgressRootSkippedLanes
workInProgressRootInterleavedUpdatedLanes
workInProgressRootRenderPhaseUpdatedLanes
workInProgressRootPingedLanes
Ok, no clues what they are for now, but workInProgressRootRenderLanes
looks simple to dive into.
// The lanes we're renderinglet workInProgressRootRenderLanes: Lanes = NoLanes;
// The lanes we're renderinglet workInProgressRootRenderLanes: Lanes = NoLanes;
As the comment says itself, this is the lanes weāre rendering.
There are a few places it is used, for example here:
export function requestUpdateLane(fiber: Fiber): Lane {// Special casesconst mode = fiber.mode;if ((mode & ConcurrentMode) === NoMode) {return (SyncLane: Lane);} else if (!deferRenderPhaseUpdateToNextBatch &&(executionContext & RenderContext) !== NoContext &&workInProgressRootRenderLanes !== NoLanes) {// This is a render phase update. These are not officially supported. The// old behavior is to give this the same "thread" (lanes) as// whatever is currently rendering. So if you call `setState` on a component// that happens later in the same render, it will flush. Ideally, we want to// remove the special case and treat them as if they came from an// interleaved event. Regardless, this pattern is not officially supported.// This behavior is only a fallback. The flag only exists until we can roll// out the setState warning, since existing code might accidentally rely on// the current behavior.return pickArbitraryLane(workInProgressRootRenderLanes);}// Updates originating inside certain React methods, like flushSync, have// their priority set by tracking it with a context variable.//// The opaque type returned by the host config is internally a lane, so we can// use that directly.// TODO: Move this type conversion to the event priority module.const updateLane: Lane = (getCurrentUpdatePriority(): any);if (updateLane !== NoLane) {return updateLane;}// This update originated outside React. Ask the host environment for an// appropriate priority, based on the type of event.//// The opaque type returned by the host config is internally a lane, so we can// use that directly.// TODO: Move this type conversion to the event priority module.const eventLane: Lane = (getCurrentEventPriority(): any);return eventLane;}
export function requestUpdateLane(fiber: Fiber): Lane {// Special casesconst mode = fiber.mode;if ((mode & ConcurrentMode) === NoMode) {return (SyncLane: Lane);} else if (!deferRenderPhaseUpdateToNextBatch &&(executionContext & RenderContext) !== NoContext &&workInProgressRootRenderLanes !== NoLanes) {// This is a render phase update. These are not officially supported. The// old behavior is to give this the same "thread" (lanes) as// whatever is currently rendering. So if you call `setState` on a component// that happens later in the same render, it will flush. Ideally, we want to// remove the special case and treat them as if they came from an// interleaved event. Regardless, this pattern is not officially supported.// This behavior is only a fallback. The flag only exists until we can roll// out the setState warning, since existing code might accidentally rely on// the current behavior.return pickArbitraryLane(workInProgressRootRenderLanes);}// Updates originating inside certain React methods, like flushSync, have// their priority set by tracking it with a context variable.//// The opaque type returned by the host config is internally a lane, so we can// use that directly.// TODO: Move this type conversion to the event priority module.const updateLane: Lane = (getCurrentUpdatePriority(): any);if (updateLane !== NoLane) {return updateLane;}// This update originated outside React. Ask the host environment for an// appropriate priority, based on the type of event.//// The opaque type returned by the host config is internally a lane, so we can// use that directly.// TODO: Move this type conversion to the event priority module.const eventLane: Lane = (getCurrentEventPriority(): any);return eventLane;}
Aha, notice it is requestUpdateLane()
? It is
a bit hard to understand what is going on in above function,
but it is clear that current rendering lanes somewhat affect the lanes
scheduled while rendering.
Letās go back to performConcurrentWorkOnRoot()
.
js
// Determine the next lanes to work on, using the fields stored// on the root.let lanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
js
// Determine the next lanes to work on, using the fields stored// on the root.let lanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
This decides what lanes would be worked on,
getNextLanes()
is pretty complex, weāll skip here,
just keep in mind that for the very basic cases getNextLanes()
picks up the the lane of highest priority.
js
if (lanes === NoLanes) {// Defensive coding. This is never expected to happen.return null;}// We disable time-slicing in some cases: if the work has been CPU-bound// for too long ("expired" work, to prevent starvation), or we're in// sync-updates-by-default mode.// TODO: We only check `didTimeout` defensively, to account for a Scheduler// bug we're still investigating. Once the bug in Scheduler is fixed,// we can remove this, since we track expiration ourselves.const shouldTimeSlice =!includesBlockingLane(root, lanes) &&!includesExpiredLane(root, lanes) &&(disableSchedulerTimeoutInWorkLoop || !didTimeout);let exitStatus = shouldTimeSlice? renderRootConcurrent(root, lanes): renderRootSync(root, lanes);
js
if (lanes === NoLanes) {// Defensive coding. This is never expected to happen.return null;}// We disable time-slicing in some cases: if the work has been CPU-bound// for too long ("expired" work, to prevent starvation), or we're in// sync-updates-by-default mode.// TODO: We only check `didTimeout` defensively, to account for a Scheduler// bug we're still investigating. Once the bug in Scheduler is fixed,// we can remove this, since we track expiration ourselves.const shouldTimeSlice =!includesBlockingLane(root, lanes) &&!includesExpiredLane(root, lanes) &&(disableSchedulerTimeoutInWorkLoop || !didTimeout);let exitStatus = shouldTimeSlice? renderRootConcurrent(root, lanes): renderRootSync(root, lanes);
Interesting, we can see that even in concurrent mode, it might fall back to sync mode in some cases. For example, if the lanes includes blocking lanes or some lanes are expired.
Letās skip the details and move on.
4. updateReducer()
As we have explained before,
useState()
is mapped to mountState()
for initial render
and updateState()
in the following updates.
The state update happens in updateState()
.
js
function updateState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {return updateReducer(basicStateReducer, (initialState: any));}
js
function updateState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>] {return updateReducer(basicStateReducer, (initialState: any));}
Internally updateReducer()
is used. (source)
function updateReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: (I) => S): [S, Dispatch<A>] {const hook = updateWorkInProgressHook();const queue = hook.queue;queue.lastRenderedReducer = reducer;const current: Hook = (currentHook: any);// The last rebase update that is NOT part of the base state.let baseQueue = current.baseQueue;// The last pending update that hasn't been processed yet.const pendingQueue = queue.pending;if (pendingQueue !== null) {// We have new updates that haven't been processed yet.// We'll add them to the base queue.if (baseQueue !== null) {// Merge the pending queue and the base queue.const baseFirst = baseQueue.next;const pendingFirst = pendingQueue.next;baseQueue.next = pendingFirst;pendingQueue.next = baseFirst;}current.baseQueue = baseQueue = pendingQueue;queue.pending = null;}if (baseQueue !== null) {// We have a queue to process.const first = baseQueue.next;let newState = current.baseState;let newBaseState = null;let newBaseQueueFirst = null;let newBaseQueueLast = null;let update = first;do {const updateLane = update.lane;if (!isSubsetOfLanes(renderLanes, updateLane)) {// Priority is insufficient. Skip this update. If this is the first// skipped update, the previous update/state is the new base// update/state.} else {// This update does have sufficient priority.}update = update.next;} while (update !== null && update !== first);if (newBaseQueueLast === null) {newBaseState = newState;} else {newBaseQueueLast.next = (newBaseQueueFirst: any);}// Mark that the fiber performed work, but only if the new state is// different from the current state.if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();}hook.memoizedState = newState;hook.baseState = newBaseState;hook.baseQueue = newBaseQueueLast;queue.lastRenderedState = newState;}return [hook.memoizedState, dispatch];}
function updateReducer<S, I, A>(reducer: (S, A) => S,initialArg: I,init?: (I) => S): [S, Dispatch<A>] {const hook = updateWorkInProgressHook();const queue = hook.queue;queue.lastRenderedReducer = reducer;const current: Hook = (currentHook: any);// The last rebase update that is NOT part of the base state.let baseQueue = current.baseQueue;// The last pending update that hasn't been processed yet.const pendingQueue = queue.pending;if (pendingQueue !== null) {// We have new updates that haven't been processed yet.// We'll add them to the base queue.if (baseQueue !== null) {// Merge the pending queue and the base queue.const baseFirst = baseQueue.next;const pendingFirst = pendingQueue.next;baseQueue.next = pendingFirst;pendingQueue.next = baseFirst;}current.baseQueue = baseQueue = pendingQueue;queue.pending = null;}if (baseQueue !== null) {// We have a queue to process.const first = baseQueue.next;let newState = current.baseState;let newBaseState = null;let newBaseQueueFirst = null;let newBaseQueueLast = null;let update = first;do {const updateLane = update.lane;if (!isSubsetOfLanes(renderLanes, updateLane)) {// Priority is insufficient. Skip this update. If this is the first// skipped update, the previous update/state is the new base// update/state.} else {// This update does have sufficient priority.}update = update.next;} while (update !== null && update !== first);if (newBaseQueueLast === null) {newBaseState = newState;} else {newBaseQueueLast.next = (newBaseQueueFirst: any);}// Mark that the fiber performed work, but only if the new state is// different from the current state.if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();}hook.memoizedState = newState;hook.baseState = newBaseState;hook.baseQueue = newBaseQueueLast;queue.lastRenderedState = newState;}return [hook.memoizedState, dispatch];}
A big one, but letās just focus on the core part
js
do {const updateLane = update.lane;if (!isSubsetOfLanes(renderLanes, updateLane)) {// Priority is insufficient. Skip this update. If this is the first// skipped update, the previous update/state is the new base// update/state....}update = update.next;} while (update !== null && update !== first);
js
do {const updateLane = update.lane;if (!isSubsetOfLanes(renderLanes, updateLane)) {// Priority is insufficient. Skip this update. If this is the first// skipped update, the previous update/state is the new base// update/state....}update = update.next;} while (update !== null && update !== first);
Yeah, it loops through the updates and checks the lanes by isSubsetOfLanes
,
renderLanes
is set in renderWithHooks()
,
tracing back, the root function call is in performUnitOfWork()
.
js
next = beginWork(current, unitOfWork, subtreeRenderLanes);
js
next = beginWork(current, unitOfWork, subtreeRenderLanes);
Phew, end of talk. This is a lot so far, weāve seen how the lanes work roughly.
5. Summary
- when events happen on fibers, updates are created with Lane info, which is determined by a few factors.
- ancestor fibers are marked with
childLanes
, so for any fiber, we can get the lane info for descendant nodes. - get the highest priority lanes from root ā map it to scheduler priority ā schedule a task in scheduler to reconcile the fiber tree
- in reconciliation, pick the highest priority lanes to work on - current rendering lanes
- traverse thought the fiber tree, check the updates on hooks, run the updates that have lanes included in the rendering lanes.
Thus we are able to run separately for multiple updates on single fiber.
6. But whatās the point of Lanes? Letās look at some example.
A demo explains more than a thousand words.
6.1 Demo - Inputs blocked by rendering long list
Open the first demo, type something in the input, you can feel the lagging, input field is not responding.
It is laggy because we enforced the delay for the rendering of each cell.
function Cell() {const start = Date.now();while (Date.now() - start < 1) {}return <span className={`cell ${COLORS[Math.round(Math.random())]}`} />;}function _Cells() {return (<div className="cells">{new Array(1000).fill(0).map((_, index) => (<Cell key={index} />))}</div>);}const Cells = React.memo(_Cells);function App() {const [text, setText] = useState("");return (<div className="app"><inputtype="text"value={text}onChange={(e) => {setText(e.target.value);}}/><Cells text={text} /></div>);}
function Cell() {const start = Date.now();while (Date.now() - start < 1) {}return <span className={`cell ${COLORS[Math.round(Math.random())]}`} />;}function _Cells() {return (<div className="cells">{new Array(1000).fill(0).map((_, index) => (<Cell key={index} />))}</div>);}const Cells = React.memo(_Cells);function App() {const [text, setText] = useState("");return (<div className="app"><inputtype="text"value={text}onChange={(e) => {setText(e.target.value);}}/><Cells text={text} /></div>);}
We can improve the case by separating the lanes for updating <Cells>
, by using startTransition()
, see the second demo.
6.2 Demo - Input not blocked by moving heavy work to transition lanes
Open the second demo to give it a try
We can see that the input is instantly responding, while the cells are rendered later.
This is because we moved the update of <Cells/>
to transition lanes.
function App() {const [text, setText] = useState("");const deferredText = React.useDeferredValue(text);return (<div className="app"><inputtype="text"value={text}onChange={(e) => {setText(e.target.value);}}/><Cells text={deferredText} /></div>}
function App() {const [text, setText] = useState("");const deferredText = React.useDeferredValue(text);return (<div className="app"><inputtype="text"value={text}onChange={(e) => {setText(e.target.value);}}/><Cells text={deferredText} /></div>}
The trick here is useDeferredValue()
, which puts updates in transition lanes.
For details of this built-in API, check out this episode -
How does React.useDeferredValue() work internally?
Also open the devtool, you can see the difference of these two.
For the first one:
js
pendingLanes 0000000000000000000000000000001pendingLanes 0000000000000000000000000000001performSyncWorkOnRoot()lanes to work on 0000000000000000000000000000001workLoopSyncpendingLanes 0000000000000000000000000000000pendingLanes 0000000000000000000000000000000
js
pendingLanes 0000000000000000000000000000001pendingLanes 0000000000000000000000000000001performSyncWorkOnRoot()lanes to work on 0000000000000000000000000000001workLoopSyncpendingLanes 0000000000000000000000000000000pendingLanes 0000000000000000000000000000000
You can see there is only one lane - SyncLane, so the update for the input and the cells are processed in the same batch.
While for the second demo situation is a bit different.
js
pendingLanes 0000000000000000000000000000001pendingLanes 0000000000000000000000000000001performSyncWorkOnRoot()lanes to work on 0000000000000000000000000000001workLoopSyncpendingLanes 0000000000000000000000001000000pendingLanes 0000000000000000000000001000000pendingLanes 0000000000000000000000001000000performConcurrentWorkOnRoot()pendingLanes 0000000000000000000000001000000
js
pendingLanes 0000000000000000000000000000001pendingLanes 0000000000000000000000000000001performSyncWorkOnRoot()lanes to work on 0000000000000000000000000000001workLoopSyncpendingLanes 0000000000000000000000001000000pendingLanes 0000000000000000000000001000000pendingLanes 0000000000000000000000001000000performConcurrentWorkOnRoot()pendingLanes 0000000000000000000000001000000
We can see there are two passes, first is SyncLane for the input, but for the Cells, it is TransitionLane1
.
6.3 Demo - use the internal API to schedule.
My 3rd demo is also easy to understand.
function App() {const [num, setNum] = React.useState(1);const renders = React.useRef([]);renders.current.push(num);return (<div><buttononClick={() => {setCurrentUpdatePriority(4);setNum((num) => num + 1);setCurrentUpdatePriority(1);setNum((num) => num * 10);}}>click me</button>{renders.current.map((log, i) => (<p key={i}>{log}</p>))}</div>);}
function App() {const [num, setNum] = React.useState(1);const renders = React.useRef([]);renders.current.push(num);return (<div><buttononClick={() => {setCurrentUpdatePriority(4);setNum((num) => num + 1);setCurrentUpdatePriority(1);setNum((num) => num * 10);}}>click me</button>{renders.current.map((log, i) => (<p key={i}>{log}</p>))}</div>);}
We called seState()
twice at the same time, but each with different update priority (lane), first call is InputContinuousLane
, the second one is SyncLane
.
So what do you expect for the result ?
If we donāt consider priority, we might think they are process together so 1 -> 20
.
Actual result is 1 -> 10 -> 20
.
Open devtool and click the button, we would see what is going on
bash
pendingLanes 0000000000000000000000000000100pendingLanes 0000000000000000000000000000101pendingLanes 0000000000000000000000000000101performSyncWorkOnRoot()lanes to work on 0000000000000000000000000000001workLoopSyncrender App() with state 10pendingLanes 0000000000000000000000000000100pendingLanes 0000000000000000000000000000100performConcurrentWorkOnRoot()pendingLanes 0000000000000000000000000000100lanes to work on 0000000000000000000000000000100shouldTimeSlice falseworkLoopSyncrender App() with state 20pendingLanes 0000000000000000000000000000000pendingLanes 0000000000000000000000000000000
bash
pendingLanes 0000000000000000000000000000100pendingLanes 0000000000000000000000000000101pendingLanes 0000000000000000000000000000101performSyncWorkOnRoot()lanes to work on 0000000000000000000000000000001workLoopSyncrender App() with state 10pendingLanes 0000000000000000000000000000100pendingLanes 0000000000000000000000000000100performConcurrentWorkOnRoot()pendingLanes 0000000000000000000000000000100lanes to work on 0000000000000000000000000000100shouldTimeSlice falseworkLoopSyncrender App() with state 20pendingLanes 0000000000000000000000000000000pendingLanes 0000000000000000000000000000000
First we processed SyncLane, so 1 * 10 = 10
,
then process the rest lanes,
notice SyncLane hook update still needs to be run for the consistency, so (1 + 1) * 10 = 20
.
Thatās it for this episode, hope it helps you understand better of React internals.
Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!