Lanes

March 20, 20255 min read

Not all updates are equally urgent. Typing in an input field needs to feel instant. Updating a filtered list behind that input can wait a frame.

Before React 18, React treated every setState call as equally urgent. Everything went into the same queue, and everything rendered together. Lanes are how React learned to disagree.

What Are Lanes?

A Lane is a bitmask — a number that represents a priority level. React assigns every update to a lane when it's scheduled.

Plain text
SyncLane = 0b0000000000000000000000000000001 (most urgent) InputContinuousLane= 0b0000000000000000000000000000100 DefaultLane = 0b0000000000000000000000000010000 TransitionLane1 = 0b0000000000000000000000010000000 (less urgent) IdleLane = 0b0100000000000000000000000000000 (least urgent)

React can check which lanes have pending work using bitwise operations — extremely fast. You'll never write these yourself. They exist under the hood so React can compare priorities in a single CPU instruction rather than walking arrays or objects.

The Six Lane Categories

There are six categories of lanes. Only two are in your hands.

LaneWho controls itWhat it's for
SyncReact (you can use flushSync)Opt out of Fiber entirely — synchronous, blocking render
Input / User ActionsReactClicks, key presses, drag, scroll — must feel instant
DefaultReactNormal setState calls
TransitionYou — via startTransitionWork that's important but not urgent
RetryReactFailed renders that need another attempt
IdleReact (not public yet)Off-screen components, hidden UI

The ones you control are Transition lanes. Everything else React assigns automatically.

SyncLane and flushSync

flushSync lets you opt out of Fiber's cooperative scheduling entirely — it forces a synchronous, blocking render immediately. My take: if someone tells you they need it, ask them why seven times. You will almost always find there is a better way. It exists, but you can safely pretend it doesn't.

Retry and Idle Lanes

Retry lanes are for renders that previously failed and are being given another shot at lower priority.

Idle lanes are for off-screen APIs (Activity, offscreen) that are not yet public. They handle completely hidden UI that shouldn't compete with anything visible. If you're reading this when those APIs are stable — this was written before they shipped, not because the idea was wrong.

How Updates Get Their Lane

React assigns lanes based on how an update is triggered:

TriggerLaneWhy
onClick, onKeyDown (discrete events)SyncUser gesture — must feel instant
onMouseMove, onScroll (continuous events)InputContinuousNeeds to be smooth
Normal setStateDefaultStandard priority
startTransitionTransitionDeveloper-marked as deferrable
useDeferredValueTransitionSame — React defers the derived update

Everything Is Urgent by Default

Here's the key mental shift: you don't mark things as urgent. You mark things as not urgent.

If you do nothing, every update is urgent. React processes them all in the same high-priority lane. If everything is urgent, nothing is urgent — you lose the ability to prioritise.

startTransition is how you opt a state update out of urgency:

TSX
import { useState, startTransition } from 'react'; function SearchPage() { const [input, setInput] = useState(''); const [results, setResults] = useState(allItems); function handleChange(e) { // Urgent: update the input immediately setInput(e.target.value); // Non-urgent: filter can wait startTransition(() => { setResults(allItems.filter(item => item.name.toLowerCase().includes(e.target.value.toLowerCase()) )); }); } return ( <> <input value={input} onChange={handleChange} /> <HeavyList items={results} /> </> ); }

What React does:

  1. Immediately commits the input update (SyncLane) — input feels responsive
  2. Starts working on the list update (TransitionLane)
  3. If the user types again before the list update finishes — React discards the in-progress list render and starts fresh with the newer input value

The list render is interruptible. The input render is not. This is lanes in action.

What "Interruptible" Actually Looks Like

Diagram

The user never waits for the list to catch up. They feel the input responding instantly.

Lanes and the isPending Flag

When you use useTransition (the hook version of startTransition), you get an isPending flag:

TSX
const [isPending, startTransition] = useTransition(); // isPending is true while the transition render is in progress return ( <> <input onChange={...} /> {isPending && <Spinner />} {/* show while list is computing */} <HeavyList items={results} /> </> );

This is how you give users feedback ("results updating...") without blocking their interaction.

The Mental Model

Think of React's work queue as a hospital triage system:

  • Sync (chest pain) — treated immediately, never waits
  • Default (broken arm) — normal queue, processed in order
  • Transition (follow-up appointment) — important, but can wait if something urgent comes in
  • Idle (routine checkup) — only done when nothing else is pending

The doctor (React) always handles the most urgent patient first. A transition render can be pre-empted by an incoming sync update and has to restart from scratch when the urgent work is done.

What Lanes Cannot Do

Lanes don't make your code faster. A heavy computation still takes the same amount of CPU time. What lanes change is when that time is paid — not during a user gesture, but between frames when the user isn't waiting.

If your list filter takes 500ms, startTransition doesn't reduce that to 50ms. It just ensures the input update (instant) lands before the list update (500ms later, non-blocking).

For truly expensive computations, you still need useMemo, web workers, or virtualisation. Lanes manage scheduling — not computation time.

Practice what you just read.

startTransition in Action
1 exercise