What you'll learn: By the end of this lesson you will be able to explain what a fiber is, how React's work loop can pause, abandon and restart a render, how lanes give updates a priority, what
useTransition,useDeferredValueand Suspense change about that, and which problems concurrent rendering does not solve.
Picture a short-order kitchen with one cook. A customer orders a stew that takes an hour, and a second customer asks for a coffee. A kitchen that insists on finishing the stew first makes the coffee customer wait. A better kitchen prepares the stew on a side counter in small steps, makes the coffee when it is asked for, and puts the stew on a table only when it is complete. If the stew order changes halfway, the cook throws the half-made stew away and starts again.
That is the shape of concurrent rendering in React. It is not parallelism: there is still one cook, the browser's main thread (see The Event Loop and the Main Thread). It arranges work so that urgent updates do not wait behind slow ones.
The running example is a product search page: a text box and a list of 5,000 products (an illustrative size). Each row is slow enough that re-rendering the whole list on every keystroke makes typing stutter. At the time of writing, react.dev lists React 19.3 as the latest version. The site itself runs React 18.3, and everything here applies to 18 unless it says otherwise.
The core idea
- Rendering and committing are different steps. Rendering is React calling your components to work out what the screen should show. Committing is applying the result to the DOM. React can pause or discard a render, because nothing is visible until the commit. (The lesson on render and commit phases is planned and not yet published.)
- A fiber is a unit of work. React turns your component tree into a tree of fiber objects and walks it one fiber at a time. Between fibers, the concurrent work loop can ask "should I stop now?"
- Priorities decide what to work on. Updates get a lane. A key press is urgent, a transition is not. React works on the urgent one first and may throw away non-urgent work in progress.
The React 18 announcement says concurrency "is a new behind-the-scenes mechanism that enables React to prepare multiple versions of your UI at the same time". It is opt-in, "only enabled when you use a concurrent feature". Without one, updates render "in a single, uninterrupted, synchronous transaction". These features also need the createRoot API.
Fiber: a unit of work you can pause
In React's source (ReactFiber.js on the main branch at the time of writing), a fiber has, among other fields, type, key, stateNode, the tree pointers return, child and sibling, pendingProps, memoizedProps, memoizedState, lanes and alternate. The widely read architecture notes by a React team member call a fiber a "unit of work", and say they are not an official document and may be out of date.
Figure 1. A fiber tree and its pointers
How to read this: child and sibling let the work loop walk down and across, and return lets it climb back up. React can stop after any fiber and resume from the pointers alone, with no JavaScript call stack to remember where it was.
Each component instance has at most two fibers: the current one on screen and a work in progress linked through alternate. The source comment says React uses "a double buffering pooling technique because we know that we'll only ever need at most two versions of a tree." Rendering builds the new tree on the side, the kitchen's side counter, and the commit swaps it in. A function component's hook state hangs off its fiber's memoizedState; see How Hooks Work: State, Ordering and the Rules of Hooks.
The work loop and the scheduler
The reconciler has two loops. The synchronous workLoopSync is commented "Perform work without checking if we need to yield between fiber". The concurrent one is:
// ReactFiberWorkLoop.js, main branch at the time of writing
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress); // "Perform work until Scheduler asks us to yield."
}
performUnitOfWork processes one fiber. When shouldYield() returns true, the loop returns with the render unfinished, and the main thread can run input and paint before React comes back. This is the yielding of the event loop lesson, applied to rendering.
shouldYield comes from the separate scheduler package. Its source says that by default it yields several times per frame. It schedules its own tasks with setImmediate where available, otherwise a MessageChannel, and uses setTimeout only outside browsers. The comment explains the choice: "We prefer MessageChannel because of the 4ms setTimeout clamping", the clamp described in the event loop lesson.
Figure 2. From a set call to the screen
How to read this: the loop and the scheduler trade turns. Each yield lets the browser handle input before the next scheduler task continues the render. Only a finished tree reaches the commit.
Not every render yields. In performWorkOnRoot, time slicing is off for a forced synchronous render, for a blocking lane, and for lanes that have expired (the comment says this prevents starvation). In the source the blocking lanes are the hydration, continuous-input, default and gesture lanes. So an ordinary setState in a click handler is not time-sliced. Slicing is for transitions, deferred values and retries.
Lanes: how updates get a priority
A lane is a bit in a bit mask. The source defines SyncLane, DefaultLane, a group of TransitionLanes, RetryLanes, IdleLane and OffscreenLane, and picks the most urgent pending one with lanes & -lanes. Each queued update records its lane (the Update type in ReactFiberHooks.js has a lane field), and lanes expire, with shorter times for sync and continuous-input lanes than for default and transition lanes. The values live in feature flags, so this lesson quotes none.
useTransition is how you give an update a transition lane. In the source, startTransition raises the priority of the pending flag, sets a transition context while your callback runs, and restores both afterwards.
Interruption and restart
When a higher-priority update arrives mid-render, React does not resume the old work. The comment in renderRootConcurrent reads: "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." The useTransition page says the same in visible terms: if you update a chart in a transition and type into an input mid-render, "React will restart the rendering work on the chart component after handling the input update."
Figure 3. Typing interrupts a transition render
How to read this: the input is never behind the list. The price is that a list render may be wasted, so the work in it must be safe to repeat.
What that means for your code:
- Render must be pure. A render can run, be abandoned and run again before any commit. In development, Strict Mode double-calls components and updaters to expose impurity.
- Effects run after the commit. The background re-render of a deferred value "does not fire Effects until it's committed to the screen".
- The DOM stays consistent. React "waits to perform DOM mutations until the end, once the entire tree has been evaluated".
- A component function cannot be paused. React yields between fibers, not inside one. This is inferred from the loop structure, not stated in the docs: one very slow component stays slow, and slicing helps only when work is spread across many fibers.
Three tools, three situations
Illustration 1, minimal: useTransition when you own the set call
Split the search box into an urgent value (the input) and a non-urgent one (the list):
function Search() {
const [text, setText] = useState(""); // urgent: the input
const [query, setQuery] = useState(""); // non-urgent: the list
const [isPending, startTransition] = useTransition();
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
setText(e.target.value);
startTransition(() => setQuery(e.target.value));
}
return (
<div>
<input value={text} onChange={onChange} />
<Results query={query} /> {/* Results is wrapped in memo */}
</div>
);
}
A test run under jsdom with React 19.3.1 (a DOM simulation, so it shows the order of renders, not timing) logged, after typing "a": Search renders: text="a" query="" isPending=true, then Search renders: text="a" query="a" isPending=false. The urgent render comes first, with the new text, the old list and a pending flag. The transition render follows. Per react.dev, isPending stays true "until all Actions complete and the final state is shown".
Illustration 2, realistic: useDeferredValue when you only receive a value
If the term arrives as a prop, you cannot wrap the set call. Defer the value instead:
function Results({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
return <SlowList text={deferredQuery} />; // SlowList is wrapped in memo
}
The docs: React "will first attempt a re-render with the old value", then "try another re-render in the background with the new value". The same jsdom run, with the parent holding query, logged App renders: query="ab" deferred="a" with SlowList not called, then App renders: query="ab" deferred="ab" and SlowList renders with "ab". The background render is interruptible, has "no fixed delay", and "does not by itself prevent extra network requests".
Illustration 3, failure: no memo, no benefit
"This optimization requires SlowList to be wrapped in memo." Without it, SlowList re-renders in the urgent pass with the old value, so typing stays slow. In the same jsdom run with memo removed, the log showed SlowList renders with "a" a second time before the render with "ab". Deferring never makes a render faster: "it tells React that re-rendering the list can be deprioritized".
Figure 4. Which tool for which problem
one huge component?"} -->|"yes"| F["Reduce the work:
virtualise, paginate, worker"] Q1 -->|"no, many fibers"| Q2{"Do you own the
set call?"} Q2 -->|"yes"| T["useTransition or startTransition"] Q2 -->|"no, it is a prop or hook value"| D["useDeferredValue + memo on the slow child"] T --> N["Keep text input state urgent"] D --> N
How to read this: concurrent features reorder work, they do not remove it. Ask first whether the work can be split, then pick the tool by what you control.
Suspense, and external stores
A Suspense boundary shows its fallback while its children are not ready. Whether an update that suspends shows it depends on the update's priority.
Figure 5. What a boundary does when new content suspends
How to read this: the same data delay gives two experiences. On the lower path the user keeps the old screen, and isPending can dim it.
The Suspense page: "React will only prevent unwanted fallbacks during non-urgent updates. It will not delay a render if it's the result of an urgent update." A transition "only waits long enough to avoid hiding already revealed content", and a newly rendered boundary still shows its fallback at once. Suspense activates for lazy components and for cached promises read with use, not for data fetched in an Effect. React 19 adds async functions in transitions ("Actions"); one documented limit remains: state updates after an await need another startTransition.
Because a render can span several tasks, a mutable store could change under a half-finished render. useSyncExternalStore handles this: if a store changes during a transition, React "will fall back to performing that update as blocking".
Trade-offs
| Approach | What it gives | What it costs |
|---|---|---|
| Do nothing | Simplest code | Urgent updates wait behind slow renders |
useTransition |
Urgent and non-urgent split, isPending |
You must own the set call; extra state; wasted renders when interrupted |
useDeferredValue |
Works from a prop; no fixed delay | Needs memo on the slow child; an extra background render |
| Debounce | Fewer renders | A fixed delay for everyone; each render still blocks |
| Virtualise or paginate | Less work in total | Layout and accessibility work |
In practice: a search page that drops keystrokes
This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company or product, and every number in it is an illustrative assumption.
The situation
A catalogue page filters 5,000 products (illustrative) as the user types. On the team's laptops it feels fine. Field data from mid-range phones shows poor Interaction to Next Paint on this page, and the slow part is the list render after each key press. Three engineers own the page.
Starting architecture
Figure 6. One urgent path for everything
How to read this: the input cannot show the new letter until the whole list has rendered, because both share one urgent update.
Options considered
| Option | Fixes | Costs | Risk |
|---|---|---|---|
| Debounce the query | Fewer list renders | A delay for everyone | Each render is still slow |
Transition for the list query, memo on the list |
Input stays instant; list follows | A second state value | Wasted renders under fast typing |
useDeferredValue inside the list |
Same, without changing who owns state | Needs memo |
Looks like a fix even when memo is missing |
| Virtualise the list | Far fewer rows rendered | Layout and accessibility work | Larger change |
The decision
| Field | Decision record |
|---|---|
| Context | One urgent render updates the input and 5,000 rows, so typing waits for the list. |
| Decision | Keep the input state urgent, update the list's query in a transition, wrap the list in memo. Track virtualisation as a follow-up. |
| Consequences, good | Typing stays responsive; the list may lag slightly. |
| Consequences, bad | Two state values, a pending style, and renders that may be discarded. |
| Revisit when | Field INP stays above the "good" threshold: reduce the work (virtualise) instead of reordering it. |
Rollout and verification
- Record a profile with CPU throttling and check that the first urgent commit is short.
- Type quickly and confirm the list always settles on the final text.
- Ship behind a flag and compare field INP for both groups.
The 2 a.m. failure
A week later the page gains server-side suggestions in a Suspense boundary, read with use. Support reports that the results area flashes a spinner on every keystroke on slow networks. The query was passed straight to the suspending component in an urgent update, so React showed the fallback each time, as the Suspense docs warn.
First three checks: (1) confirm the suspending component receives the urgent value; (2) look for use on a new promise each render (React 19 warns about uncached promises); (3) check that isPending is used somewhere. Mitigation: disable suggestions behind the flag. Follow-up: feed the suspending child a deferred value, and add a test that the fallback never appears after the first reveal.
Architect's notes
- Decide what is urgent before choosing an API. Input, focus and press feedback are urgent; results derived from them usually are not.
- Find out whether the slow part is many fibers or one. Concurrent rendering helps the first and not the second.
- Treat
memoas part of the pattern, and keep renders pure.
Common mistakes
- Putting a controlled text input's state in a transition. The docs say transition updates "can't be used to control text inputs".
- Expecting a transition to speed up a render. It reorders work.
- Assuming a deferred value stops network requests. It defers display only.
- Reading mutable external state during render instead of using
useSyncExternalStore.
Key takeaways
- Concurrent rendering pauses, abandons and restarts renders; the commit still applies to the DOM at once.
- A fiber is a unit of work; each component instance has a current and a work-in-progress fiber.
- The concurrent loop checks
shouldYield()between fibers; blocking lanes, such as ordinary click updates, are not time-sliced. - A higher-priority update makes React throw out the render in progress and start fresh.
useTransitionsplits updates you own;useDeferredValuedoes it for values you receive, and needsmemo.- A transition or deferred value keeps already revealed content instead of a Suspense fallback; an urgent update does not.
Practice exercises
Exercise 1, Starter (30 min): Take a component with a slow child and an input. Add memo and useDeferredValue, log renders as in this lesson, predict the log, then compare.
Exercise 2, Intermediate (2 hours): Build the search page with urgent and transition state. Record a throttled profile before and after. Then move the input's own state into the transition and describe what changes and why the docs advise against it.
Exercise 3, Advanced (half day): Add a Suspense boundary reading a cached promise with use. Show the fallback flashing with an urgent update, fix it with a transition or deferred value, and write a test that the fallback is not shown after the first reveal.
Related reading
In this course:
- The Event Loop and the Main Thread, tasks, yielding and why long tasks hurt input.
- How Hooks Work: State, Ordering and the Rules of Hooks, the update queue and where hook state lives.
- Loading Performance and Core Web Vitals, how Interaction to Next Paint is measured.
Official documentation
- React v18.0 announcement, concurrency, interruptible rendering, transitions.
- React reference: useTransition
- React reference: useDeferredValue
- React reference: Suspense
- React reference: useSyncExternalStore
- React source: ReactFiberWorkLoop.js, the work loops and
performWorkOnRoot.