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
js
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)
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 1s.
js
js
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
js
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 nextLanesof 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
js
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.
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.
As the comment says itself, this is the lanes weāre rendering.
There are a few places it is used, for example here:
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
js
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
js
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
js
Internally updateReducer() is used. (source)
A big one, but letās just focus on the core part
js
js
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
js
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.
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.
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
js
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
js
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.
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
bash
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!
