Skip to content
React rc performance 5 min read

Concurrent Rendering

Concurrent rendering is the engine introduced in React 18 that lets React prepare multiple versions of the UI at the same time and decide which work to commit first. Before it, rendering was a single, synchronous, all-or-nothing pass: once React started updating, it could not stop until the whole tree was done, which meant a large update could block typing, clicking, or scrolling. With concurrent rendering, React can pause, interrupt, abandon, and resume work in the background, so urgent interactions stay snappy even while expensive updates are in flight.

What “concurrent” actually means

Concurrent rendering does not introduce threads or parallelism — JavaScript is still single-threaded. Instead, React breaks rendering into small units of work and yields back to the browser between them. This makes rendering interruptible: if a more important update arrives (like a keystroke), React can drop the half-finished low-priority render, handle the urgent one, and start the original work over.

The key shift is that a render is no longer a commitment. React can compute a tree in memory without showing it, throw it away, or replace it with a newer one. Nothing is painted to the screen until React commits, and the commit phase is still synchronous and atomic — the user never sees a partially updated UI.

Priorities and interruptibility

Internally React assigns each update a priority. Urgent updates (driven by direct user input) preempt non-urgent ones. You do not set numeric priorities yourself; you express intent through concurrent features, and React maps that intent onto its scheduler.

Update kindPriorityExample
UrgentHighTyping in an input, clicking a button, dragging
TransitionLow, interruptibleFiltering a large list, switching tabs/routes
DeferredLow, lags behindA heavy view derived from a fast-changing value

When a high-priority update interrupts a transition, React keeps the old committed UI visible until the new tree is fully ready, rather than flashing an inconsistent intermediate state.

You opt in through features, not directly

There is no enableConcurrentRendering() switch in application code. Concurrent rendering is turned on simply by rendering with the React 18+ root API, and you then opt in to interruptibility per update using the concurrent features.

import { createRoot } from "react-dom/client";
import App from "./App.jsx";

// The concurrent-capable root. Everything rendered here can use
// concurrent features; legacy ReactDOM.render did not.
createRoot(document.getElementById("root")).render(<App />);

The three primary opt-ins are useTransition, useDeferredValue, and <Suspense>. Each tells React “this particular work is interruptible and lower priority than user input.”

Marking work as a transition

startTransition flags state updates as non-urgent so the input stays responsive while an expensive re-render happens in the background.

import { useState, useTransition } from "react";

function SearchBox({ allItems }) {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState(allItems);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    const next = e.target.value;
    setQuery(next); // urgent: keep the input snappy

    startTransition(() => {
      // low priority: can be interrupted by the next keystroke
      const filtered = allItems.filter((item) =>
        item.toLowerCase().includes(next.toLowerCase())
      );
      setResults(filtered);
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <span>Updating…</span>}
      <ul>
        {results.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

If the user types three characters quickly, React can abandon the first two filtered renders and only commit the result for the final value — wasted work is thrown away instead of janking the typing.

Deferring a derived value

useDeferredValue lets a heavy subtree lag behind a fast-changing source value, so the source updates immediately and the expensive part catches up.

import { useDeferredValue, useMemo } from "react";

function ResultsView({ query, allItems }) {
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(
    () =>
      allItems.filter((item) =>
        item.toLowerCase().includes(deferredQuery.toLowerCase())
      ),
    [deferredQuery, allItems]
  );

  const isStale = query !== deferredQuery;

  return (
    <ul style={{ opacity: isStale ? 0.6 : 1 }}>
      {results.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

Tip: useTransition is for updates you trigger from an event handler; useDeferredValue is for values you receive as props or state and want to “follow at a distance.” Reach for the deferred value when you do not own the update site.

How Suspense ties in

Concurrent rendering is what makes <Suspense> cooperative. When a component suspends (because data or code is still loading), React can keep the previous content on screen and continue rendering the rest of the tree, instead of immediately replacing everything with a fallback. Combined with transitions, navigating to a new route can avoid showing a jarring spinner if the new screen is almost ready.

import { Suspense } from "react";

function Page() {
  return (
    <Suspense fallback={<Spinner />}>
      <SlowProfile />
    </Suspense>
  );
}

What it does not do

Concurrent rendering does not make slow components fast — a render that takes 200ms still takes 200ms of CPU; it is just chopped up and reprioritized. It also does not run effects or fetch data in parallel. You still need memoization, virtualization, and code-splitting for the underlying cost. Concurrency improves responsiveness, not raw throughput.

Warning: Because low-priority renders can run, be discarded, and re-run, your render function must stay pure. Side effects during render (mutating shared objects, logging counters, writing to refs) can run an unexpected number of times. Keep effects in useEffect.

Best practices

  • Render the app with createRoot so concurrent features are available — the legacy root silently disables them.
  • Wrap expensive, non-urgent state updates in startTransition; keep the value the user directly edits as an urgent update.
  • Use useDeferredValue when the expensive work depends on a prop or state you do not control directly.
  • Show a lightweight pending indicator with isPending instead of blocking the UI behind a spinner.
  • Keep render functions pure so interrupted and replayed renders behave identically.
  • Treat concurrency as a responsiveness tool, not a substitute for memo, useMemo, virtualization, and smaller bundles.
Last updated June 14, 2026
Was this helpful?