7 min read
0%

Concurrent rendering

Back to Blog
Concurrent rendering

Concurrent Rendering

Concurrent rendering lets React prepare multiple versions of the UI at the same time and decide which to show based on priority. It’s the foundational shift that makes Transitions, Suspense, and deferred rendering possible.

What Changed in React 18

Prior to React 18, all rendering was synchronous. Once React started a render, it ran to completion, blocking the main thread.

React 18’s concurrent renderer can:

  • Pause a render mid-tree if higher-priority work arrives
  • Resume from where it paused
  • Abandon a render and restart with newer state
  • Render multiple trees concurrently (e.g., a Transition update alongside a sync update)

Opt in by switching to the concurrent root:

// React 17 (legacy, synchronous)
ReactDOM.render(<App />, root);

// React 18 (concurrent)
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Priority Lanes

Every update is assigned a lane (priority level):

SyncLane           - user input (highest)
InputContinuousLane - drag, scroll
DefaultLane        - fetch, setTimeout
TransitionLane     - startTransition (lowest)

React processes higher lanes first. If a low-priority render is in progress and a high-priority update arrives, React interrupts the low-priority work, handles the high-priority update synchronously, then resumes (or restarts) the low-priority work.

What “Interruptible” Means

The render phase is split into discrete units of work (fiber nodes). Between each unit, React checks a deadline. If it should yield, React parks its work pointer, hands control back to the browser, then resumes via a scheduler callback.

The commit phase remains synchronous and cannot be interrupted.

Concurrent Features

startTransition: marks an update as non-urgent.

startTransition(() => setSearchResults(computeHeavyFilter(input)));

useDeferredValue: creates a deferred copy of a value that lags behind urgent updates.

const deferredQuery = useDeferredValue(query);

useTransition: same as startTransition but gives you an isPending flag.

const [isPending, startTransition] = useTransition();

Tearing

Concurrent rendering introduces tearing: if an external store is mutated between render units, different fibers may read different versions. Fix with useSyncExternalStore:

const value = useSyncExternalStore(store.subscribe, store.getState);

When Concurrency Doesn’t Help

Concurrent rendering doesn’t make individual computations faster. If a single component takes 200ms to render synchronously, concurrency can’t break that up - it can only yield between fiber units, not within one.


Canvas is not supported in your browser