cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Lanes: how React prioritizes updates

Read ReactFiberLane at v19.3.1: lanes as bits, how an update gets its lane, how getNextLanes picks what renders, interruption, entanglement and expiration.

By the end, you can
  • 01
    Read a lanes numberTurn a value like root.pendingLanes into the lanes it holds, and pick the most urgent one with lanes & -lanes.
  • 02
    Predict an update's laneSay which lane requestUpdateLane gives a set call from a click, a pointer move, a timer or a transition.
  • 03
    Explain what renders nextUse getNextLanes to explain batching, interruption, entanglement and expired lanes in what users see.

One click, two lanes, two renders

The fiber lesson showed that a set function marks a lane on its fiber: a bit in a number that says how urgent the update is. What setState really does followed one update to the root. This lesson shows what React does with those bits once several are waiting. Press Add tea below, and read the log under the result.

One click that makes an urgent update and a transitionPop out in the code editor (opens in a new tab)JSX
import { startTransition, useLayoutEffect, useState } from "react";import { bits, clearNotes, fiberRoot, note } from "./lanes.js"; export default function App() {  const [count, setCount] = useState(0);  const [tip, setTip] = useState("Add a tea to get a tip.");   useLayoutEffect(() => {    if (count === 0) return;    const pending = bits(fiberRoot().pendingLanes);    note(`commit: cart ${count}, tip "${tip}", pending ${pending}`);  });   function addTea() {    clearNotes();    setCount(count + 1);    startTransition(() => setTip(`Tea number ${count + 1} goes well with cake.`));    note(`after the click: pending ${bits(fiberRoot().pendingLanes)}`);  }   return (    <>      <button onClick={addTea}>Add tea</button>      <p>Teas in the cart: {count}</p>      <p>{tip}</p>    </>  );}
ResultLoading…

The lanes.js tab has three helpers. fiberRoot() (lines 3 to 7) finds the object React keeps for #root, the FiberRoot from the fiber lesson, through a property React puts on the container. bits (lines 10 to 12) writes a number in binary, and note (lines 19 to 22) adds a line to the log. They read React's private fields, which is fine for learning and never fine in an app. Now App.jsx:

  1. Line 16 calls setCount in a click: an urgent update. Line 17 calls setTip inside startTransition: an update that can wait (the transitions lesson).
  2. Line 18 reads root.pendingLanes right away, before React renders anything: after the click: pending 100000010. Two bits are on. Bit 1, the second from the right, is SyncLane (2). Bit 8 is TransitionLane1 (256). Together they make 258.
  3. Lines 8 to 12 log after every commit. The first commit shows cart 1 with the old tip, and pending 100000000: the click's lane is done, and the transition's lane still waits.
  4. The second commit shows the new tip and pending 000000000: nothing left.

Click again. This time the first line reads pending 1000000010: the transition got the next lane, bit 9, TransitionLane2 (512). The picture shows the first click's number as a row of switches.

root.pendingLanes equals 258, drawn as nine switches for bits 8 down to 0. Bit 8 and bit 1 are on, the rest are off. Bit 8 is Transition 1, worth 256. Bit 5 is Default, 32. Bit 3 is Input continuous, 8. Bit 1 is Sync, 2. An arrow pointing right says: more urgent.root.pendingLanes = 258180706050403021100bitTransition 1256Default32Input continuous8Sync2can waitmore urgent
After the click, two switches are on: bit 1, SyncLane (2), and bit 8, TransitionLane1 (256). Together they make 258. Bits further right are more urgent. 22 more lanes sit to the left of these nine.
Real-life analogyLanes on a motorway

At a toll plaza, cars line up in lanes. One lane is kept for ambulances, others are for cars and slow trucks. The gatekeeper looks at which lanes have cars and opens the most urgent one first, and every car in that lane goes through together. Two trailers hitched together always cross at once. A truck that has waited far too long is waved through, whatever else is waiting.

In real life: Cars waiting at the toll gates, each in its lane
In React: Updates waiting, each lane's bit on in root.pendingLanes
In real life: The fast lane on the far right, for ambulances
In React: SyncLane, for clicks and typing
In real life: A slow lane for trucks that can wait
In React: Transition lanes, for startTransition
In real life: The gatekeeper opens the most urgent lane that has cars
In React: getNextLanes picks the lowest bit that is on
In real life: Two trailers hitched together cross at the same time
In React: Entangled lanes always render together
In real life: A truck that waited too long gets waved through
In React: An expired lane renders without stopping
The main idea

A lane is one bit of a 31-bit number, and each bit is a priority. Every update gets a lane when you call a set function. The root keeps the lanes of all waiting updates in root.pendingLanes. Before each render, getNextLanes picks the most urgent of them, and the render works only on updates in those lanes. The others wait for a later render.

A lane is one bit of a number

React could have kept a list of updates with a priority on each. It keeps numbers instead, and uses the bitwise operators on them. Here is every helper it needs, from ReactFiberLane.js:

The lane helpers, ReactFiberLane.js lines 756 to 798 (shortened)JavaScript
export function getHighestPriorityLane(lanes: Lanes): Lane {
  return lanes & -lanes;
}
// ... (helpers that find a lane's position left out)
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane): boolean {
  return (a & b) !== NoLanes;
}

export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane): boolean {
  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;
}
  1. Lines 1 to 3: lanes & -lanes keeps only the lowest bit that is on. -lanes flips every bit and adds 1, so the only bit both numbers share is the lowest one. The lowest bit is the most urgent lane.
  2. Lines 5 to 7: & keeps the bits two numbers share. If any is left, the two sets of lanes overlap.
  3. Lines 9 to 11: subset is inside set when & keeps all of its bits.
  4. Lines 13 to 15: | switches on every bit that is on in either number. That is how lanes join a set.
  5. Lines 17 to 19: ~subset flips the subset's bits, and & with it switches them off.

Each helper is one operator or two, however many updates are waiting, and a set of lanes is a single number that fits in a fiber's field. Try the same operations yourself. Press Edit & run and change the numbers.

Merge, test, remove and pick lanesPop out in the code editor (opens in a new tab)JavaScript
const SyncLane = 0b10;
const DefaultLane = 0b100000;
const TransitionLane1 = 0b100000000;

let pending = SyncLane | TransitionLane1;
console.log(pending, pending.toString(2));
console.log((pending & DefaultLane) !== 0);
console.log(pending & -pending);
pending &= ~SyncLane;
console.log(pending.toString(2));
console.log(1 << 30, 1 << 31);

Line 5 merges two lanes: 258 100000010. Line 7 asks whether DefaultLane is in the set: false. Line 8 picks the most urgent lane: 2. Line 9 removes SyncLane, and line 10 prints 100000000. Line 11 shows why there are 31 lanes and not 32: bitwise operators work on 32-bit whole numbers (the numbers in depth lesson), and the 32nd bit is the sign. 1 << 30 is 1073741824, but 1 << 31 is -2147483648. With 31 lanes every set of lanes stays positive, so loops like while (lanes > 0) and comparisons like nextLane >= wipLane work.

ReactFiberLane.js declares the lanes as binary numbers, from line 41 to line 110:

The lane groups, ReactFiberLane.js lines 41 to 110 (shortened)JavaScript
export const TotalLanes = 31;
// ... (the six lanes for events, which the fiber lesson showed, left out)
export const SyncUpdateLanes: Lane =
  SyncLane | InputContinuousLane | DefaultLane;
// ... (GestureLane and TransitionHydrationLane left out)
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111100000000;
const TransitionLane1: Lane = /*                        */ 0b0000000000000000000000100000000;
// ... (TransitionLane2 to TransitionLane14, and the groups they form, left out)
const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;
// ... (RetryLane1 to RetryLane4 and SelectiveHydrationLane left out)
const NonIdleLanes: Lanes = /*                          */ 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
export const IdleLane: Lane = /*                        */ 0b0010000000000000000000000000000;

export const OffscreenLane: Lane = /*                   */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /*                    */ 0b1000000000000000000000000000000;

Line 1 sets the count. Lines 3 and 4 group the three lanes that updates outside a transition use (events, timers and the like); the next sections show why that group matters. Lines 6 and 7 show the 14 transition lanes as one mask, and the first of them. Line 9 is the four retry lanes, line 11 every lane that isn't idle, and lines 13 to 17 the idle, offscreen and deferred lanes at the far left. Each lane that does urgent work has a hydration twin just to its right, used while React attaches to HTML that came from the server. How to read the table: each row is a group of lanes, the bits they use, and what puts updates in them.

The 31 lanes
LanesBitsValuesWhat puts work there
SyncHydrationLane, SyncLane0, 11, 2Discrete events, such as clicks, key presses and typing, and flushSync.
InputContinuousHydrationLane, InputContinuousLane2, 34, 8Continuous events, such as pointer moves, scrolling and dragging.
DefaultHydrationLane, DefaultLane4, 516, 32Updates with no event running: timers, network callbacks, and root.render.
GestureLane664Gesture transitions, an experimental feature that is off in 19.3.1.
TransitionHydrationLane, TransitionLane1 to TransitionLane147, then 8 to 21128, then 256 to 2097152startTransition uses lanes 1 to 10. The background render of useDeferredValue uses lanes 11 to 14.
RetryLane1 to RetryLane422 to 254194304 to 33554432Showing a Suspense boundary's content once the data it waited for has arrived.
SelectiveHydrationLane2667108864Hydrating part of a server-rendered page ahead of the rest, because the user is about to use it.
IdleHydrationLane, IdleLane27, 28134217728, 268435456Work that can wait until nothing else is pending.
OffscreenLane29536870912Rendering hidden content in the background, such as a hidden Activity.
DeferredLane301073741824A marker for deferred work. It never renders on its own.

Where an update's lane comes from

The What setState really does lesson showed the lanes a click, a timer and a transition get. Here is the code that picks them. Every set function asks requestUpdateLane which lane its update goes in:

requestUpdateLane in ReactFiberWorkLoop.js, lines 812 to 868 (shortened)JavaScript
export function requestUpdateLane(fiber: Fiber): Lane {
  // ... (legacy roots, and set calls made during a render, left out)
  const transition = requestCurrentTransition();
  if (transition !== null) {
    // ... (a check for gestures, and development-only bookkeeping, left out)
    return requestTransitionLane(transition);
  }

  const priority = resolveUpdatePriority();
  // ... (a development-only line left out)
  return eventPriorityToLane(priority);
}
  1. Lines 3 to 7: inside startTransition, there is a current transition, and the update gets a transition lane.
  2. Lines 9 and 11: otherwise, the lane is an event priority. Event priorities are lanes under another name, in ReactEventPriorities.js:
Event priorities are lanes, ReactEventPriorities.js lines 24 to 28JavaScript
export const NoEventPriority: EventPriority = NoLane;
export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;

So eventPriorityToLane on line 11 hands the priority back unchanged. Where the priority comes from is react-dom's job, in resolveUpdatePriority:

resolveUpdatePriority in ReactDOMUpdatePriority.js, lines 35 to 45JavaScript
export function resolveUpdatePriority(): EventPriority {
  const updatePriority = ReactDOMSharedInternals.p; /* currentUpdatePriority */
  if (updatePriority !== NoEventPriority) {
    return updatePriority;
  }
  const currentEvent = window.event;
  if (currentEvent === undefined) {
    return DefaultEventPriority;
  }
  return getEventPriority(currentEvent.type);
}
  1. Lines 2 to 5: while React runs your event handler, it has already set the priority for that event, so that wins. (So does flushSync, which sets SyncLane.)
  2. Lines 6 to 9: otherwise React looks at window.event, the event the browser is handling right now. No event, as in a timer or a network callback, means DefaultEventPriority: DefaultLane.
  3. Line 10: an event means its type decides, in getEventPriority:
getEventPriority in ReactDOMEventListener.js, lines 312 to 416 (shortened)JavaScript
export function getEventPriority(domEventName: DOMEventName): EventPriority {
  switch (domEventName) {
    // Used by SimpleEventPlugin:
    case 'beforetoggle':
    case 'cancel':
    case 'click':
    // ... (50 more cases, such as input, keydown, pointerdown and submit)
      return DiscreteEventPriority;
    case 'drag':
    // ... (18 more cases, such as mousemove, pointermove, scroll and wheel)
      return ContinuousEventPriority;
    // ... (the message event, which asks the scheduler for its priority, left out)
    default:
      return DefaultEventPriority;
  }
}

Lines 4 to 8: discrete events, ones that happen once each, like a click, a key press or typing, give SyncLane. Lines 9 to 11: continuous events, which fire many times a second, like moving the pointer or scrolling, give InputContinuousLane. Line 14: every other event gives DefaultLane. Try each control below. Move the pointer across the bars too.

The same set function, called from four placesPop out in the code editor (opens in a new tab)JSX
import { startTransition, useState } from "react";import { bits, fiberRoot } from "./lanes.js"; export default function App() {  const [volume, setVolume] = useState(5);  const [seen, setSeen] = useState("Try each control.");   function report(where) {    setSeen(`${where}: pending ${bits(fiberRoot().pendingLanes)}`);  }   function louder() {    setVolume(Math.min(10, volume + 1));    report("click");  }   function slideTo(level) {    setVolume(level);    report("pointermove");  }   function fadeInLater() {    setTimeout(() => {      setVolume(10);      report("setTimeout");    }, 300);  }   function loadPreset() {    startTransition(() => setVolume(3));    report("startTransition");  }   return (    <>      <div className="levels" aria-label="Volume bars">        {Array.from({ length: 11 }, (_, level) => (          <span key={level} className={level <= volume ? "on" : ""} onPointerMove={() => slideTo(level)} />        ))}      </div>      <p>Volume: {volume}</p>      <button onClick={louder}>Louder</button>      <button onClick={fadeInLater}>Fade in later</button>      <button onClick={loadPreset}>Load a preset</button>      <p>{seen}</p>    </>  );}
ResultLoading…

Line 9 reads pendingLanes right after each set call, before React renders. Louder (line 13) reports click: pending 000000010. Moving over the bars (line 38) reports pointermove: pending 000001000, bit 3. Fade in later calls setVolume from a timer (lines 23 to 26), when no event is running: setTimeout: pending 000100000, bit 5. Load a preset (line 30) reports startTransition: pending 100000000 the first time; press it again and the bit moves one place left.

A set call, setVolume of 3, goes to a question card: inside startTransition? An arrow labeled yes goes left to a card: a transition lane, with 256, 512, 1024 and so on under it. An arrow labeled no goes right to a card: window.event. Under it, three lines: click, keydown or input gives 2. pointermove or scroll gives 8. No event gives 32.setVolume(3)inside startTransition?yesa transition lane256, 512, 1024, ...nowindow.eventclick, keydown: 2pointermove, scroll: 8no event: 32
requestUpdateLane asks two questions. Inside startTransition, the update gets a transition lane. Otherwise, the event the browser is handling decides: a discrete event gives SyncLane, a continuous one InputContinuousLane, and no event at all DefaultLane.

The moving bit comes from requestTransitionLane:

requestTransitionLane in ReactFiberRootScheduler.js, lines 711 to 723JavaScript
  if (currentEventTransitionLane === NoLane) {
    // All transitions within the same event are assigned the same lane.
    const actionScopeLane = peekEntangledActionLane();
    currentEventTransitionLane =
      actionScopeLane !== NoLane
        ? // We're inside an async action scope. Reuse the same lane.
          actionScopeLane
        : // We may or may not be inside an async action scope. If we are, this
          // is the first update in that scope. Either way, we need to get a
          // fresh transition lane.
          claimNextTransitionUpdateLane();
  }
  return currentEventTransitionLane;

Line 1: this event has no transition lane yet, so lines 3 to 11 pick one, and line 13 returns it. The comment on line 2 is the rule: every transition in the same event shares one lane. Lines 5 to 7: while an async action is still running (a startTransition whose function is async and hasn't finished), new transitions reuse its lane. Otherwise line 11 claims a fresh one, from claimNextTransitionUpdateLane:

claimNextTransitionUpdateLane in ReactFiberLane.js, lines 726 to 736JavaScript
export function claimNextTransitionUpdateLane(): Lane {
  // Cycle through the lanes, assigning each new transition to the next lane.
  // In most cases, this means every transition gets its own lane, until we
  // run out of lanes and cycle back to the beginning.
  const lane = nextTransitionUpdateLane;
  nextTransitionUpdateLane <<= 1;
  if ((nextTransitionUpdateLane & TransitionUpdateLanes) === NoLanes) {
    nextTransitionUpdateLane = TransitionLane1;
  }
  return lane;
}

Line 6 shifts the next lane one bit left, and lines 7 to 9 start again at TransitionLane1 after TransitionLane10. (Lanes 11 to 14 are for the background render of useDeferredValue.) One more detail: a promise that settles at once, like await null in a click handler, goes on while the browser is still handling that click, so window.event is still the click and the update gets SyncLane. An await that waits for the network goes on in a later task, with no event: DefaultLane.

getNextLanes picks what to render next

Every update marks its lane in root.pendingLanes (the What setState really does lesson follows that step). Before a render, React calls getNextLanes to choose which of them to work on:

The start of getNextLanes, ReactFiberLane.js lines 249 to 286 (shortened)JavaScript
export function getNextLanes(
  root: FiberRoot,
  wipLanes: Lanes,
  rootHasPendingCommit: boolean,
): Lanes {
  // Early bailout if there's no pending work left.
  const pendingLanes = root.pendingLanes;
  if (pendingLanes === NoLanes) {
    return NoLanes;
  }
  // ... (reading the suspended, pinged and warm lanes left out)
  // Do not work on any idle work until all the non-idle work has finished,
  // even if the work is suspended.
  const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
  if (nonIdlePendingLanes !== NoLanes) {
    // First check for fresh updates.
    const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
    if (nonIdleUnblockedLanes !== NoLanes) {
      nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
  1. Lines 7 to 10: nothing pending, nothing to render.
  2. Lines 14 and 15: if anything other than idle work is pending, idle work waits. That is how IdleLane and OffscreenLane stay behind everything else.
  3. Lines 17 to 19: leave out lanes that are suspended, waiting for data (the Suspense lesson), and ask getHighestPriorityLanes which of the rest go first.
getHighestPriorityLanes in ReactFiberLane.js, lines 180 to 225 (shortened)JavaScript
function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
  const pendingSyncLanes = lanes & SyncUpdateLanes;
  if (pendingSyncLanes !== 0) {
    return pendingSyncLanes;
  }
  switch (getHighestPriorityLane(lanes)) {
    // ... (each event lane and hydration lane returns itself)
    case TransitionLane1:
    case TransitionLane2:
    case TransitionLane3:
    case TransitionLane4:
    case TransitionLane5:
    case TransitionLane6:
    case TransitionLane7:
    case TransitionLane8:
    case TransitionLane9:
    case TransitionLane10:
      if (enableParallelTransitions) {
        return getHighestPriorityLane(lanes);
      }
      return lanes & TransitionUpdateLanes;
    case TransitionLane11:
    case TransitionLane12:
    case TransitionLane13:
    case TransitionLane14:
      return lanes & TransitionDeferredLanes;
    case RetryLane1:
    case RetryLane2:
    case RetryLane3:
    case RetryLane4:
      return lanes & RetryLanes;
  1. Lines 2 to 5: if any of SyncLane, InputContinuousLane and DefaultLane are pending, all of them that are go together. A click, a pointer move and a timer's update that are all waiting render in one batch.
  2. Line 6 takes the most urgent lane, the lowest bit, and the switch decides what joins it.
  3. Lines 8 to 19: a transition lane renders alone. enableParallelTransitions is true in ReactFeatureFlags.js at 19.3.1, so two transitions in different lanes render one after the other. Line 21 is the other branch: with the flag off, all pending transition lanes would render together.
  4. Lines 22 to 26 and 27 to 31: the deferred-value lanes go together, and so do the retry lanes.

How to read the table: each row says what happens when the most urgent pending lane is in one group.

What getHighestPriorityLanes returns
The most urgent pending laneWhat rendersWhat you see
SyncLane, InputContinuousLane or DefaultLaneEvery pending lane of those three, together.A click, a pointer move and a timer's update that are all waiting show in one commit.
TransitionLane1 to TransitionLane10That one lane, plus any lane entangled with it.Transitions on different state commit one after the other.
TransitionLane11 to TransitionLane14Every pending lane of those four.The background renders of useDeferredValue that are waiting commit together.
RetryLane1 to RetryLane4Every pending retry lane.Suspense boundaries whose data has arrived show their content together.
IdleLane or OffscreenLaneThat one lane, and only when no other lane is pending.Hidden content renders after everything you can see.
Two rows. Top row: pending 100000010, an arrow to render lanes 000000010, an arrow to what you see: cart 1. A curved arrow goes down to the bottom row: pending 100000000, an arrow to render lanes 100000000, an arrow to what you see: the tip.root.pendingLaneslanes it renderson screen100000010000000010cart 1100000000100000000the tip
The overview's click, as two renders. getNextLanes picks SyncLane first, so render 1 shows the cart while the tip waits. After that commit only TransitionLane1 is pending, and render 2 shows the tip.

Now try it. The board starts with the overview's click: SyncLane and TransitionLane1. Switch lanes on and off. Then pick something under Rendering now, and tick the box at the bottom.

Playground: the lane board
root.pendingLanes258
0000000000000000000000100000010
SyncLane, TransitionLane1
Most urgent, pending & -pending: 2 SyncLane
The next render2
0000000000000000000000000000010
SyncLane
Loop: workLoopSync
Try it yourself
Rendering now:
Render next: SyncLane

getNextLanes picks the urgent lanes: SyncLane. TransitionLane1 waits for a later render. It has a blocking lane, so it renders with workLoopSync, without stopping.

A model written for this lesson, not a view inside React: it picks lanes the way getNextLanes, getHighestPriorityLanes and getEntangledLanes do in React 19.3.1 for a root with nothing suspended, and the lesson's test checks every combination against those real functions.

Try these: switch on A click, A pointer move and A timer together, and all three render at once. With only the two transitions on, the next render has just TransitionLane1. Tick the box, and it includes TransitionLane2 as well; the entangling section explains why. Turn everything off except Idle work and A hidden Activity: the idle lane goes first, and it is time sliced.

Two transitions on different state really do render one after the other. This button starts one, and a timer starts the next a moment later:

Two transitions on different statePop out in the code editor (opens in a new tab)JSX
import { memo, startTransition, useLayoutEffect, useState } from "react";import { bits, clearNotes, fiberRoot, note } from "./lanes.js"; function busy(ms) {  const end = performance.now() + ms;  while (performance.now() < end) {}} function SlowPart() {  busy(2); // part of a slow page  return null;} const Shelf = memo(function Shelf({ name, page }) {  return (    <p>      {name}: page {page}      {Array.from({ length: 50 }, (_, n) => <SlowPart key={n} />)}    </p>  );}); export default function App() {  const [teaPage, setTeaPage] = useState(1);  const [cakePage, setCakePage] = useState(1);   useLayoutEffect(() => {    if (teaPage + cakePage === 2) return;    const pending = bits(fiberRoot().pendingLanes);    note(`commit: teas page ${teaPage}, cakes page ${cakePage}, pending ${pending}`);  });   function nextTeas() {    startTransition(() => setTeaPage((page) => page + 1));    note(`teas transition: pending ${bits(fiberRoot().pendingLanes)}`);  }   function nextCakes() {    startTransition(() => setCakePage((page) => page + 1));    note(`cakes transition: pending ${bits(fiberRoot().pendingLanes)}`);  }   function both() {    clearNotes();    nextTeas();    setTimeout(nextCakes, 0);  }   return (    <>      <button onClick={both}>Next teas, then next cakes</button>      <Shelf name="Teas" page={teaPage} />      <Shelf name="Cakes" page={cakePage} />    </>  );}
ResultLoading…

Each Shelf renders 50 slow parts, about 2 ms each (lines 9 to 12 and 18). Line 45 starts the teas transition, and line 46 starts the cakes transition from a timer. The log shows teas transition: pending 100000000, then cakes transition: pending 1100000000: two transition lanes. The first commit shows teas page 2, cakes page 1, pending 1000000000: the cakes waited for their own render, which the last line shows. Click again and the bits move further left, because each transition claims the next lane.

An urgent lane interrupts a transition

The transitions lesson showed that typing interrupts a slow transition. Here is the check that decides it, the end of getNextLanes. wipLanes are the lanes of the render in progress:

getNextLanes keeps a render or interrupts it, ReactFiberLane.js lines 334 to 361JavaScript
  // If we're already in the middle of a render, switching lanes will interrupt
  // it and we'll lose our progress. We should only do this if the new lanes are
  // higher priority.
  if (
    wipLanes !== NoLanes &&
    wipLanes !== nextLanes &&
    // If we already suspended with a delay, then interrupting is fine. Don't
    // bother waiting until the root is complete.
    (wipLanes & suspendedLanes) === NoLanes
  ) {
    const nextLane = getHighestPriorityLane(nextLanes);
    const wipLane = getHighestPriorityLane(wipLanes);
    if (
      // Tests whether the next lane is equal or lower priority than the wip
      // one. This works because the bits decrease in priority as you go left.
      nextLane >= wipLane ||
      // Default priority updates should not interrupt transition updates. The
      // only difference between default updates and transition updates is that
      // default updates do not support refresh transitions.
      (nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
    ) {
      // Keep working on the existing in-progress tree. Do not interrupt.
      return wipLanes;
    }
  }

  return nextLanes;
}
  1. Lines 4 to 9: a render is in progress, and the lanes just picked are different from its lanes.
  2. Lines 11 and 12 take the most urgent lane of each.
  3. Line 16: if the new lane is the same bit or further left, it is not more urgent, so lines 22 and 23 keep the render going.
  4. Line 20: a DefaultLane update, such as one from a timer, never interrupts a transition either.
  5. Line 27: otherwise the new lanes win.

When the lanes change, the render in progress is thrown away. Both render functions start with the same check, here in renderRootSync:

renderRootSync starts over for new lanes, ReactFiberWorkLoop.js lines 2633 to 2654 (shortened)JavaScript
  // If the root or lanes have changed, throw out the existing stack
  // and prepare a fresh one. Otherwise we'll continue where we left off.
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    // ... (DevTools bookkeeping left out)
    workInProgressTransitions = getTransitionsForLanes(root, lanes);
    prepareFreshStack(root, lanes);
  }

Line 3: a different root or different lanes than the ones in progress. Line 6: prepareFreshStack, from the work loop lesson, starts a new tree from the root, and the half-built one is dropped. Watch it happen. Type green quickly in the box below.

A search box: urgent text, results in a transitionPop out in the code editor (opens in a new tab)JSX
import { memo, startTransition, useEffect, useLayoutEffect, useState } from "react";import { bits, clearNotes, fiberRoot, note } from "./lanes.js"; const teas = ["green", "grey", "ginger", "genmaicha", "gunpowder", "jasmine", "mint", "sencha"];const names = teas.flatMap((tea) => [1, 2, 3, 4].map((n) => `${tea} blend ${n}`)); function busy(ms) {  const end = performance.now() + ms;  while (performance.now() < end) {}} function Result({ name }) {  busy(8); // a slow row  return <li>{name}</li>;} const Results = memo(function Results({ query }) {  note(`render results for "${query}"`);  useLayoutEffect(() => note(`commit results for "${query}"`), [query]);  const found = query ? names.filter((name) => name.includes(query)) : [];  return <ul>{found.map((name) => <Result key={name} name={name} />)}</ul>;}); export default function App() {  const [text, setText] = useState("");  const [query, setQuery] = useState("");   useEffect(() => clearNotes(), []);  useLayoutEffect(() => {    if (text) note(`commit the box: "${text}"`);  }, [text]);   function type(event) {    setText(event.target.value);    startTransition(() => setQuery(event.target.value));    note(`typed "${event.target.value}": pending ${bits(fiberRoot().pendingLanes)}`);  }   return (    <>      <input value={text} onChange={type} placeholder="Search teas" />      <button onClick={clearNotes}>Clear the log</button>      <Results query={query} />    </>  );}
ResultLoading…

Line 34 sets the box's text, an urgent update, and line 35 sets the query in a transition. Each result row takes 8 ms (line 13), so the results for g, 20 rows, take a while. If you type r before they are done, the log reads:

  1. typed "g": pending 100000010, then commit the box: "g", then render results for "g": the urgent lane first, then the transition starts.
  2. typed "gr": pending 1100000010. SyncLane is back, so React stops the results render, commits the box, and starts over.
  3. render results for "gr" and commit results for "gr". There is never a commit results for "g": that render was thrown away.

Type slowly instead, and you'll see commit results for "g" before typed "gr", and the second line has a single transition bit. The second item hides a question: two transition bits were pending, yet the new render showed "gr" at once, not "g" first. The next section explains it. (The concurrent rendering lesson follows a thrown-away render in detail.)

Entangled lanes render together

Two lanes are entangled when React may not render one without the other. When a set function makes a transition update, it calls entangleTransitionUpdate:

entangleTransitionUpdate in ReactFiberHooks.js, lines 3867 to 3890JavaScript
function entangleTransitionUpdate<S, A>(
  root: FiberRoot,
  queue: UpdateQueue<S, A>,
  lane: Lane,
): void {
  if (isTransitionLane(lane)) {
    let queueLanes = queue.lanes;

    // If any entangled lanes are no longer pending on the root, then they
    // must have finished. We can remove them from the shared queue, which
    // represents a superset of the actually pending lanes. In some cases we
    // may entangle more than we need to, but that's OK. In fact it's worse if
    // we *don't* entangle when we should.
    queueLanes = intersectLanes(queueLanes, root.pendingLanes);

    // Entangle the new transition lane with the other transition lanes.
    const newQueueLanes = mergeLanes(queueLanes, lane);
    queue.lanes = newQueueLanes;
    // Even if queue.lanes already include lane, we don't know for certain if
    // the lane finished since the last time we entangled it. So we need to
    // entangle it again, just to be sure.
    markRootEntangled(root, newQueueLanes);
  }
}
  1. Line 7: queue.lanes holds the transition lanes this piece of state has already received.
  2. Line 14 drops the ones that are no longer pending: those transitions are finished.
  3. Lines 17 to 22 add the new lane, and markRootEntangled writes down, on the root, that each of these lanes must render with all the others.

Then, when a render starts, prepareFreshStack asks getEntangledLanes (from ReactFiberWorkLoop.js, line 2282) which lanes to add:

getEntangledLanes in ReactFiberLane.js, lines 427 to 475 (shortened)JavaScript
export function getEntangledLanes(root: FiberRoot, renderLanes: Lanes): Lanes {
  let entangledLanes = renderLanes;
  // ... (a check for InputContinuousLane, and a long comment, left out)
  const allEntangledLanes = root.entangledLanes;
  if (allEntangledLanes !== NoLanes) {
    const entanglements = root.entanglements;
    let lanes = entangledLanes & allEntangledLanes;
    while (lanes > 0) {
      const index = pickArbitraryLaneIndex(lanes);
      const lane = 1 << index;

      entangledLanes |= entanglements[index];

      lanes &= ~lane;
    }
  }

  return entangledLanes;
}

Line 7 keeps the render lanes that are entangled with something. Lines 8 to 15 visit each one, and line 12 adds every lane it is entangled with. In the search box, g and gr both set query, so their two transition lanes were entangled: the restarted render had both, and the results went straight to "gr". That is what you want from a search box: the newest query, not every one you typed on the way. In the teas and cakes example, the two transitions set different state, so nothing tied them, and they rendered one at a time. Tick the box on the lane board to see both cases.

Entanglement is not a reason to interrupt. A comment in getEntangledLanes says it is applied after the check for a render in progress. If a second transition on the same state arrives while the first one renders, and nothing urgent comes in between, the first render finishes on its own and commits, and the second one follows.

A starved lane expires

Lanes have a problem. If urgent updates keep coming, a transition could be thrown away forever. React calls that starvation, and fixes it with a deadline per lane. Each time it schedules the root, in ReactFiberRootScheduler.js, line 397, it calls markStarvedLanesAsExpired:

markStarvedLanesAsExpired in ReactFiberLane.js, lines 541 to 588 (shortened)JavaScript
export function markStarvedLanesAsExpired(
  root: FiberRoot,
  currentTime: number,
): void {
  // ... (a comment left out)
  const pendingLanes = root.pendingLanes;
  const suspendedLanes = root.suspendedLanes;
  const pingedLanes = root.pingedLanes;
  const expirationTimes = root.expirationTimes;
  // ... (a comment about retry lanes left out)
  let lanes = enableRetryLaneExpiration
    ? pendingLanes
    : pendingLanes & ~RetryLanes;
  while (lanes > 0) {
    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1 << index;

    const expirationTime = expirationTimes[index];
    if (expirationTime === NoTimestamp) {
      // Found a pending lane with no expiration time. If it's not suspended, or
      // if it's pinged, assume it's CPU-bound. Compute a new expiration time
      // using the current time.
      if (
        (lane & suspendedLanes) === NoLanes ||
        (lane & pingedLanes) !== NoLanes
      ) {
        // Assumes timestamps are monotonically increasing.
        expirationTimes[index] = computeExpirationTime(lane, currentTime);
      }
    } else if (expirationTime <= currentTime) {
      // This lane expired
      root.expiredLanes |= lane;
    }

    lanes &= ~lane;
  }
}
  1. Lines 11 to 13: every pending lane, except the retry lanes, which never expire in 19.3.1.
  2. Lines 14 to 16 visit one lane at a time.
  3. Lines 19 to 29: a lane without a deadline gets one now, unless it is waiting for data: suspended, and React hasn't been told yet that the data arrived (React calls that message a ping).
  4. Lines 30 to 33: a lane whose deadline has passed goes into root.expiredLanes.

The deadline comes from computeExpirationTime and three numbers in ReactFeatureFlags.js:

computeExpirationTime in ReactFiberLane.js, lines 477 to 530 (shortened)JavaScript
function computeExpirationTime(lane: Lane, currentTime: number) {
  switch (lane) {
    case SyncHydrationLane:
    case SyncLane:
    case InputContinuousHydrationLane:
    case InputContinuousLane:
    case GestureLane:
      // User interactions should expire slightly more quickly.
    // ... (a comment left out)
      return currentTime + syncLaneExpirationMs;
    case DefaultHydrationLane:
    case DefaultLane:
    case TransitionHydrationLane:
    case TransitionLane1:
    // ... (TransitionLane2 to TransitionLane13 left out)
    case TransitionLane14:
      return currentTime + transitionLaneExpirationMs;
    case RetryLane1:
    // ... (RetryLane2 to RetryLane4, and a comment, left out)
      return enableRetryLaneExpiration
        ? currentTime + retryLaneExpirationMs
        : NoTimestamp;
    case SelectiveHydrationLane:
    // ... (IdleHydrationLane and IdleLane left out)
    case OffscreenLane:
    case DeferredLane:
      // Anything idle priority or lower should never expire.
      return NoTimestamp;
How long lanes may wait, ReactFeatureFlags.js lines 146 to 149JavaScript
export const enableRetryLaneExpiration: boolean = false;
export const retryLaneExpirationMs = 5000;
export const syncLaneExpirationMs = 250;
export const transitionLaneExpirationMs = 5000;

How to read the table: each row is a group of lanes, how long its updates may wait, and what happens then.

Deadlines per lane
LanesHow long they may waitThen
SyncLane, InputContinuousLane, their hydration lanes, and GestureLane250 ms (syncLaneExpirationMs).The lane is marked expired. These lanes already render without yielding.
DefaultLane, its hydration lane, TransitionHydrationLane and the 14 transition lanes5,000 ms (transitionLaneExpirationMs).The lane is marked expired, so a transition renders with workLoopSync instead of in slices.
RetryLane1 to RetryLane4Forever, because enableRetryLaneExpiration is false.They are always rendered in slices.
SelectiveHydrationLane, the idle lanes, OffscreenLane and DeferredLaneForever.They wait as long as other work keeps coming.

An expired lane changes one thing. The work loop lesson showed that performWorkOnRoot only time slices when !includesExpiredLane(root, lanes) (line 1171). An expired lane renders with workLoopSync: to the end, without yielding, so nothing can interrupt it. Try it. Start renders a slow report in a transition. Start, and keep tapping also clicks Tap every 20 ms, like a very fast user.

A transition that taps keep interruptingPop out in the code editor (opens in a new tab)JSX
import { memo, startTransition, useLayoutEffect, useState } from "react"; function busy(ms) {  const end = performance.now() + ms;  while (performance.now() < end) {}} function loopOnTheStack() {  Error.stackTraceLimit = 50;  const lines = new Error().stack.split("\n");  const names = lines.map((line) => line.trim().replace(/^at /, "").split(/[ @(]/)[0]);  const loops = ["workLoopSync", "workLoopConcurrentByScheduler"];  return loops.find((name) => names.includes(name)) ?? "neither";} let lastLoop = ""; function SlowPart({ last }) {  busy(1); // part of a slow report  if (last) lastLoop = loopOnTheStack();  return null;} const Report = memo(function Report({ round }) {  return (    <p>      Report {round}      {Array.from({ length: 100 }, (_, n) => <SlowPart key={n} last={n === 99} />)}    </p>  );}); let started = 0;let tapsAtStart = 0;let tapping = null; export default function App() {  const [round, setRound] = useState(0);  const [taps, setTaps] = useState(0);  const [result, setResult] = useState("Press a Start button.");   useLayoutEffect(() => {    if (round === 0) return;    clearInterval(tapping);    const ms = Math.round(performance.now() - started);    setResult(`Report ${round} took ${ms} ms. Taps while it waited: ${taps - tapsAtStart}. Its last part rendered in ${lastLoop}.`);  }, [round]);   function start(keepTapping) {    started = performance.now();    tapsAtStart = taps;    setResult("Rendering the report...");    if (keepTapping) tapping = setInterval(() => document.getElementById("tap").click(), 20);    startTransition(() => setRound(round + 1));  }   return (    <>      <button onClick={() => start(false)}>Start</button>      <button onClick={() => start(true)}>Start, and keep tapping</button>      <button id="tap" onClick={() => setTaps(taps + 1)}>Tap ({taps})</button>      <p>{result}</p>      <Report round={round} />    </>  );}
ResultLoading…

The report has 100 parts of 1 ms each (lines 18 to 22 and 28). Lines 8 to 14 find which loop is on the stack, as in the work loop lesson, and line 20 records it for the last part. Line 53 starts a timer that clicks Tap (line 61) every 20 ms. Start finishes in a few hundred milliseconds, with no taps, and its last part rendered in workLoopConcurrentByScheduler. Start, and keep tapping reports a little over 5,000 ms, many taps, and workLoopSync: every tap threw the render away until the lane expired, and then React finished it without stopping.

A timeline. On the left, three crossed-out render cards, each followed by a tap card: each tap throws the render away. Then a dotted gap, and at 5,000 milliseconds a card: render without yielding. A time arrow runs under the whole row from 0 ms to 5,000 ms.rendertaprendertaprendertap...render, no yieldseach tap throws the render awaythe lane has expired0 ms5,000 mstime
With a tap every 20 ms, the transition never gets the time it needs: each tap is more urgent, and each one throws the render away. 5,000 ms after the update, markStarvedLanesAsExpired marks its lane expired, and React renders it to the end without yielding, so the taps wait.

Which lane will my update get?

You never pick a lane by name, but you pick one every time you choose where to call a set function. When typing feels slow, the question is: which updates are in SyncLane and render before the screen can update? Move the expensive one into startTransition and it gets a transition lane, which can be interrupted. Sort these set calls by the lane they get:

Which lane does this update get?
  • setCount in an onClick handler
  • setText in an input's onChange
  • setPosition in onPointerMove
  • setOffset in onScroll
  • setTime in a setInterval callback
  • setResults when a fetch finishes
  • setTab inside startTransition, in a click handler
  • The background render useDeferredValue starts
Try it yourself
0 of 8 correct

Put each set call under the lane React gives its update.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

To see lanes in a real app, record a Performance profile in Chrome. React's Scheduler track sorts its work into rows named by getGroupNameOfHighestPriorityLane: Blocking for the sync, continuous and default lanes, Transition, Suspense for retries, and Idle (the profiling lesson shows how to record one).

Common mix-ups

  • "A lane is a queue of updates." No. A lane is one bit. The updates wait in each hook's queue; the lanes only say which ones a render may take.
  • "A bigger lane number is more urgent." No. Smaller is more urgent: SyncLane is 2, and IdleLane is 268435456. lanes & -lanes picks the smallest.
  • "Every update gets its own render." No. Updates in the same lane render together, and so do SyncLane, InputContinuousLane and DefaultLane.
  • "All transitions render together." Not in React 19.3.1. Transitions in different lanes render one at a time, unless they set the same state and are entangled.
  • "A timer's update can interrupt a transition." No. getNextLanes never lets DefaultLane interrupt a transition. A click or typing can.
  • "A transition that keeps being interrupted never shows." No. After 5,000 ms its lane expires, and it renders without stopping.
  • "There are 32 lanes, one per bit." No. There are 31, so that every set of lanes stays a positive number.

Practice exercises

Exercise 1 · Warm-upRead a lanes number

Predict what this prints, then press Edit & run to check.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const pendingLanes = 0b100101000;
const next = pendingLanes & -pendingLanes;
console.log(pendingLanes, next);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPredict the lanes of a hover

    This app imports the same lanes.js. Predict the first line of the log when you move the pointer over Green tea, then press Edit & run to check.

    import { startTransition, useState } from "react";
    import { bits, fiberRoot, note } from "./lanes.js";
    
    export default function App() {
      const [hovered, setHovered] = useState("nothing");
      const [details, setDetails] = useState("");
    
      function preview(name) {
        setHovered(name);
        startTransition(() => setDetails(`Details for ${name}`));
        note(`pending ${bits(fiberRoot().pendingLanes)}`);
      }
    
      return (
        <>
          <p onPointerMove={() => preview("Green tea")}>Green tea</p>
          <p>Hovering: {hovered}</p>
          <p>{details}</p>
        </>
      );
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWrite the lane helpers

      Finish the four helpers with bit operators. What does the last line print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const SyncLane = 0b10;
      const DefaultLane = 0b100000;
      const TransitionLane1 = 0b100000000;
      
      function merge(a, b) {
        // Return a number with every lane of a and of b.
      }
      
      function includes(set, lane) {
        // Return true when set has the lane's bit.
      }
      
      function remove(set, lane) {
        // Return set without the lane's bit.
      }
      
      function mostUrgent(set) {
        // Return the lowest bit that is on.
      }
      
      const pending = merge(SyncLane, TransitionLane1);
      console.log(pending, includes(pending, DefaultLane), remove(pending, SyncLane), mostUrgent(pending));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeGive the results a transition lane

        Type in the box: each key waits for the slow results, and the log shows only SyncLane. Change one line so the results get a transition lane. What does the log show after you type g?

        import { memo, startTransition, useState } from "react";import { bits, fiberRoot, note } from "./lanes.js"; const names = ["green", "grey", "ginger", "jasmine", "mint", "oolong"].flatMap((tea) => [1, 2, 3, 4].map((n) => `${tea} blend ${n}`)); function busy(ms) {  const end = performance.now() + ms;  while (performance.now() < end) {}} function Result({ name }) {  busy(8); // a slow row  return <li>{name}</li>;} const Results = memo(function Results({ query }) {  const found = query ? names.filter((name) => name.includes(query)) : [];  return <ul>{found.map((name) => <Result key={name} name={name} />)}</ul>;}); export default function App() {  const [text, setText] = useState("");  const [query, setQuery] = useState("");   function type(event) {    setText(event.target.value);    setQuery(event.target.value);    note(`typed "${event.target.value}": pending ${bits(fiberRoot().pendingLanes)}`);  }   return (    <>      <input value={text} onChange={type} placeholder="Search teas" />      <Results query={query} />    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeWrite a small getNextLanes

          Write pickNext with the three rules in its comments, the ones getNextLanes follows. What do the four lines print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const SyncUpdateLanes = 0b101010; // SyncLane, InputContinuousLane and DefaultLane
          const DefaultLane = 0b100000;
          const TransitionLanes = 0b1111111111111100000000;
          
          function pickNext(pending, wip) {
            // 1. If any of SyncUpdateLanes are pending, return all of them that are.
            // 2. Otherwise, pick the most urgent single lane.
            // 3. A render of wip is in progress (0 means none): keep wip, unless the pick's
            //    most urgent lane is more urgent than wip's. A DefaultLane pick never
            //    interrupts a transition.
          }
          
          console.log(pickNext(0b100101010, 0));
          console.log(pickNext(0b1100000000, 0));
          console.log(pickNext(0b100000010, 0b100000000));
          console.log(pickNext(0b100100000, 0b100000000));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeMark starved lanes

            Finish markStarved the way markStarvedLanesAsExpired works, with a deadline per lane. What do the two lines print?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const NoTimestamp = -1;
            // How long each lane may wait: SyncLane, DefaultLane, TransitionLane1, IdleLane.
            const timeouts = { 2: 250, 32: 5000, 256: 5000, 268435456: NoTimestamp };
            const root = { pendingLanes: 0b100000010, expirationTimes: {}, expiredLanes: 0 };
            
            function markStarved(root, now) {
              // For each lane in root.pendingLanes:
              // - no deadline yet (undefined): give it now + its timeout, unless its timeout is NoTimestamp;
              // - a deadline that has come (deadline <= now): add the lane to root.expiredLanes.
            }
            
            markStarved(root, 0);
            markStarved(root, 300);
            console.log(root.expiredLanes);
            markStarved(root, 5000);
            console.log(root.expiredLanes);

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              The lanes quiz · 7 questionsScore: first tries count
              1. Question 1 of 7A click handler calls setCount, then calls startTransition with another set call. What is root.pendingLanes right after the handler?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this code print? It picks lanes the way React does.

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const pending = 0b1000101000;
                console.log(pending & -pending, pending & 0b101010);

                Choose an answer to see the explanation.

              3. Question 3 of 7A set call runs in a setTimeout callback, outside startTransition. Which lane does its update get?

                Choose an answer to see the explanation.

              4. Question 4 of 7Two transitions on different state start a moment apart, from two different tasks. How does React 19.3.1 render them?

                Choose an answer to see the explanation.

              5. Question 5 of 7In the search box, typing gr quickly never logs commit results for "g". Why?

                Choose an answer to see the explanation.

              6. Question 6 of 7A transition keeps being interrupted by clicks. What happens about 5,000 ms after its update?

                Choose an answer to see the explanation.

              7. Question 7 of 7Why does ReactFiberLane.js have 31 lanes and not 32?

                Choose an answer to see the explanation.

              Key takeaways

              • A lane is one bit of a 31-bit number. The lower the bit, the more urgent the lane.
              • requestUpdateLane gives each update a lane: a transition lane inside startTransition, otherwise the event's priority: SyncLane for clicks and typing, InputContinuousLane for moves and scrolling, DefaultLane with no event.
              • root.pendingLanes holds every waiting lane. getNextLanes picks the urgent three together, or one transition lane, or the retry lanes, and leaves idle work for last.
              • A more urgent lane interrupts a render in progress, and the unfinished work is thrown away.
              • Transitions that set the same state are entangled and render together; others render one at a time.
              • A lane that waits too long expires and renders without yielding: 250 ms for urgent lanes, 5,000 ms for transitions.

              Lanes are bits that say how urgent each update is.
              getNextLanes picks the most urgent ones, and only they render next.

              The lanes.js helper from this lesson, for your own experiments:

              lanes.jsJavaScript
              // Reads the lanes React keeps on its root. For learning only:
              // these are React's private fields, and they change between versions.
              export function fiberRoot() {
                const container = document.getElementById("root");
                const key = Object.keys(container).find((name) => name.startsWith("__reactContainer$"));
                return container[key].stateNode;
              }
              
              // A lanes number in binary, at least 9 digits long.
              export function bits(lanes) {
                return lanes.toString(2).padStart(9, "0");
              }
              
              // The log appears in a <pre> under the app, outside React's root.
              const output = document.body.appendChild(document.createElement("pre"));
              
              // Adds a line, unless it's the same as the last one: StrictMode calls
              // each component twice in a row in development.
              export function note(line) {
                const lines = output.textContent ? output.textContent.split("\n") : [];
                if (lines.at(-1) !== line) output.textContent = [...lines, line].join("\n");
              }
              
              export function clearNotes() {
                output.textContent = "";
              }

              Coming next: The scheduler package.

              CompleteFrontend Clear concepts. Working examples.