Concurrent Rendering

TL;DR / Concurrent rendering lets React prepare multiple UI updates simultaneously, pausing low-priority work to handle urgent interactions without blocking the main thread.

How It Works

  Synchronous (blocking):

  ┌────────────────────────────────────────┐
  │ Render A ████████████████████████████  │ UI frozen
  └────────────────────────────────────────┘

  Concurrent (interruptible):

  ┌──────────────────┐        ┌──────────┐        ┌──────────────────┐
  │  Render A ████   │───┐    │  Urgent  │   ┌───→│  Render A ████   │
  └──────────────────┘   └───→│  Update  │───┘    └──────────────────┘
                              │          │
                              └──────────┘


  React can pause low-priority work to
  handle urgent updates like user input.

Edit diagram

Before React 18, rendering was synchronous and uninterruptible. Once React started rendering a component tree, it would not stop until every component had been processed and the DOM updated. If rendering took 100ms, the main thread was blocked for 100ms — no input handling, no animations, no scroll events. The UI froze.

Concurrent rendering fundamentally changes this model. React can now start rendering an update, pause in the middle, do something more urgent (like handling a keystroke), and then resume or discard the paused work. Rendering becomes interruptible, resumable, and prioritizable.

The Priority System

React's concurrent renderer uses a lanes-based priority system. Each update is assigned a lane — a bit in a 31-bit bitmask — that represents its priority. Different lanes correspond to different urgency levels:

  • Sync lane — highest priority, used for discrete events like clicks. Processed synchronously.
  • Input continuous lane — for continuous events like drag, hover. High priority but can be batched.
  • Default lane — standard updates from setState calls not triggered by events.
  • Transition lane — low priority, explicitly marked with startTransition. Can be interrupted by anything.
  • Idle lane — lowest priority, processed only when the browser is idle.

When a high-priority update arrives while a lower-priority render is in progress, React interrupts the current render, processes the urgent update, commits it to the DOM, and then either resumes or restarts the lower-priority work. The user sees immediate response to their input while expensive background work continues without blocking.

Transitions: The Key API

The useTransition hook and startTransition function are how you tell React that an update is non-urgent. Wrapping a state update in startTransition moves it to the transition lane:

const [isPending, startTransition] = useTransition();

function handleSearch(query) {
  // Urgent: update the input immediately
  setInputValue(query);

  // Non-urgent: filter 10,000 items
  startTransition(() => {
    setFilteredResults(filterItems(query));
  });
}

Without the transition, both updates happen at the same priority. Typing a character triggers both the input update and the expensive filter, blocking the main thread. With the transition, the input updates immediately (sync lane), and the filter renders in the background (transition lane). If the user types another character before the filter completes, React discards the in-progress filter render and starts a new one with the latest query.

Concurrent Features Built on This Foundation

Several React features depend on concurrent rendering:

  • Suspense for data fetching — components can suspend during render, and React coordinates the loading states without blocking other parts of the tree.
  • Selective hydration — React interrupts hydration to prioritize sections the user is interacting with.
  • useDeferredValue — returns a deferred version of a value that may lag behind the current value during transitions, letting React reuse the old output while the new one renders.
  • Activity (formerly Offscreen, experimental) — pre-renders components that are not yet visible, keeping their state alive.

What Concurrent Rendering Is NOT

Concurrent rendering does not use Web Workers, threads, or parallelism. JavaScript is still single-threaded. The "concurrency" is cooperative — React voluntarily yields to the browser's event loop between units of work. It is interleaving, not parallel execution. React processes a few fiber nodes, checks if there is higher-priority work, and either continues or yields. This scheduling happens through MessageChannel (not requestIdleCallback, which was used in early experiments but proved too unpredictable).

The Double Render Concern

In concurrent mode, React may start rendering a component and then discard the work. This means render functions can execute multiple times for a single committed update. Side effects in render (network requests, mutations, subscriptions) can execute more than expected. This is why StrictMode double-invokes render functions in development — to help you catch impure renders that would break under concurrent rendering.

Gotchas

  • Concurrent rendering is opt-in per update, not per app. The concurrent renderer is always active in React 18+, but only startTransition, useDeferredValue, and Suspense actually use concurrent features. Regular setState calls still render synchronously.
  • Render functions must be pure. Because renders can be interrupted and restarted, any side effects in render (DOM mutations, subscriptions, logging) may execute unpredictably. Move side effects to useEffect.
  • useRef mutations in render are unsafe. Writing to a ref during render can cause inconsistencies when renders are discarded. Only read refs in render; write them in effects or event handlers.
  • Transition updates can be starved. If urgent updates keep arriving (rapid typing, constant mouse events), transition work gets perpetually interrupted and never commits. React has starvation prevention, but extremely high-frequency urgent updates can still cause visible delays in transition results.
  • Third-party libraries may not be concurrent-safe. Libraries that rely on synchronous rendering guarantees (render → immediate commit) may behave unexpectedly when renders are interrupted.