React Fiber, Concurrent Mode and Priorities Explained
AI generated
</>
{ }
React · Fiber · Concurrent Mode · Performance
React Fiber, Concurrent Mode and Priorities Explained
how the new reconciler keeps your UI fluid

React Fiber is not just an internal implementation detail, it is the foundation of why React 18, with Concurrent Mode, useTransition and Suspense, renders applications in a fundamentally different way than every version before it. Anyone who wants to understand why some updates are more urgent than others, and how React decides what appears on screen next, needs to know Fiber and its priority system.

15 min read Fiber · Concurrent Mode · useTransition · Suspense · Scheduler React 18+ · TypeScript

1. What React Fiber Really Is

React Fiber is the complete rewrite of the React reconciler introduced with React 16, and it remains the foundation of every rendering operation to this day. The term "Fiber" refers to two things at once: on one hand, the architecture of the new reconciler as a whole, and on the other, the individual data structures, the Fiber nodes, that represent the component tree in memory. Anyone who wants to understand how React Fiber and Concurrent Mode work first needs to know the problem Fiber solves: the main thread being blocked by synchronous rendering.

Before Fiber, React processed the entire component tree in a single, non-interruptible pass. For deep trees or computationally expensive components, this blocked the browser for hundreds of milliseconds, no input, no animations, no visual updates. Fiber solves this by splitting render work into small units that the scheduler can execute individually and interrupt at any time. That is the foundation on which Concurrent Mode and the entire priority system are built.

2. The Old Stack Reconciler and Its Limits

The original stack reconciler in React processed the component tree recursively: a component renders, calls its children, those call their children, and so on down to the leaves of the tree. This recursion was synchronous and could not be interrupted. Once it began, it had to run to completion, and during that time the browser had no way to react to user events or paint a frame. In complex applications this led to measurable jank, especially with fast typing into text fields that re-filter lists.

The fundamental problem of the stack reconciler was its execution model: call-stack-based recursion cannot be interrupted from the outside. Fiber replaces the recursion with an explicit linked list of Fiber nodes and a work loop that can cooperatively involve the scheduler. The work loop asks, after every Fiber node, whether there is still time left in the current frame, and if not, yields control back. That makes React Fiber a cooperative system rather than a preemptive one.

3. Fiber Nodes: Work Split into Interruptible Units

Every React component in the tree corresponds to a Fiber node, a JavaScript object structure with fields for type, props, state, effects, and pointers to child, sibling and parent. This linked-list structure lets the work loop pause after every node and later resume exactly where it left off. React keeps two trees at once: the current tree, which represents the visible UI, and the work-in-progress tree, in which new updates are processed. Only once the work-in-progress tree is complete are the two swapped atomically, this is called the "commit phase".

The render phase (also called the "reconciliation phase") is interruptible and can be repeated multiple times. If React discovers a more urgent update during the render phase, for example a keyboard input, it can discard the work in progress, process the urgent update synchronously, and then restart the interrupted work. This property has important consequences for side effects: nothing outside the component may be changed during the render phase, because the same work can run more than once. Side effects belong exclusively in the commit phase via useEffect.


// Understanding fiber work phases, render phase is repeatable, commit is not
import { useState, useEffect, useRef } from 'react';

function FiberPhaseDemo() {
  const [count, setCount] = useState(0);
  const commitCount = useRef(0);

  // Render phase: may run multiple times, no side effects allowed here
  const expensiveValue = computeExpensiveValue(count); // pure, idempotent

  // Commit phase: runs exactly once after React commits to DOM
  useEffect(() => {
    commitCount.current += 1;
    // Safe to interact with DOM, subscriptions, external systems here
    document.title = `Count: ${count} (commits: ${commitCount.current})`;
  }, [count]);

  return (
    <div>
      <p>Value: {expensiveValue}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

function computeExpensiveValue(n: number): number {
  // Must be pure, React Fiber may call render phase multiple times
  return n * n;
}

4. Lanes: React's Priority System

Since React 18, React Fiber uses a bitmask system called Lanes to classify updates by urgency. Each lane is a bitmask, and multiple lanes can be combined to represent groups of updates that are processed together. Synchronous updates, for example direct DOM interactions or flushSync calls, get the highest priority. Transition updates via useTransition get lower priority. Updates from setTimeout or from network responses land on even lower lanes.

The Lanes system solves a problem its predecessor (the expiration-time model) had: prioritizing groups of updates that belong together. With bitmasks, React can efficiently check which updates belong to the same group, which may be processed together, and which are more urgent than others, all without expensive data structures. For developers, the Lanes system is transparent: the public API, useTransition, useDeferredValue and startTransition, fully abstracts away the priority system.

5. Enabling and Understanding Concurrent Mode

Concurrent Mode is enabled in React 18 through the new root entry point: createRoot instead of render. This is not a gradual opt-in at the component level, once createRoot is used, Concurrent Mode applies to the entire tree. The difference from legacy mode: in legacy mode, every setState call is a synchronous, uninterruptible render. In Concurrent Mode, React can batch, interrupt and reorder multiple updates. Batching (Automatic Batching) has applied to all updates since React 18, including inside setTimeout and promise callbacks.

Automatic Batching is the first noticeable effect of Concurrent Mode in practice: multiple state updates in the same event handler result in a single re-render instead of several. In React 17 this was only true inside React event handlers, but not in timeouts and promises. With React 18, batching applies everywhere automatically. Anyone who occasionally needs a synchronous flush, for example when an update must be visible immediately before the next piece of code runs, can use flushSync from react-dom.


// React 18: createRoot enables Concurrent Mode for the entire tree
import { createRoot } from 'react-dom/client';
import { StrictMode } from 'react';
import App from './App';

// Concurrent Mode root, all features (useTransition, Suspense, etc.) available
const root = createRoot(document.getElementById('root')!);
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

// Automatic batching: both setters cause ONE re-render in React 18
async function handleSave() {
  const data = await fetchData();
  setLoading(false);   // batched
  setData(data);       // batched, single re-render
}

// Force synchronous flush when needed (e.g. measuring DOM before update)
import { flushSync } from 'react-dom';
flushSync(() => {
  setCount(c => c + 1); // renders immediately, before flushSync returns
});
const newHeight = ref.current.getBoundingClientRect().height;

6. useTransition: Urgent vs. Non-Urgent Updates

useTransition is the most important new API that makes Concurrent Mode tangible for developers. It splits UI updates into two categories: urgent and non-urgent (transition). Urgent updates must be visible immediately, input, clicks, scrolling. Non-urgent updates can wait, search results, filtered lists, page transitions. If an urgent update arrives while a transition update is in progress, React interrupts the transition update, processes the urgent update synchronously, and restarts the transition update afterward.

The pattern is simple: the state setter for the expensive operation is wrapped in startTransition. The isPending boolean from useTransition is true while processing is underway and can be used to show a loading state in the UI, without a separate loading state. The result: the text input stays responsive even when an expensive filter function runs behind it over 10,000 records. The list updates as soon as React has time, without blocking the main thread.


import { useState, useTransition, useDeferredValue } from 'react';

// Pattern 1: useTransition for explicit urgent vs. non-urgent split
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<string[]>([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    // Urgent: update input immediately
    setQuery(e.target.value);

    // Non-urgent: filtering can wait, React may interrupt and restart
    startTransition(() => {
      setResults(filterLargeDataset(e.target.value));
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} placeholder="Search…" />
      {isPending && <span>Filtering…</span>}
      <ResultList items={results} />
    </div>
  );
}

// Pattern 2: useDeferredValue, defers derived value without explicit transition
function DeferredList({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = deferredQuery !== query;

  return (
    <div style={{ opacity: isStale ? 0.6 : 1 }}>
      <ExpensiveList filter={deferredQuery} />
    </div>
  );
}

7. Suspense and Concurrent Rendering Together

Suspense has existed since React 16.6 for code splitting, but it only reaches its full potential together with Concurrent Mode. Without Concurrent Mode, the Suspense fallback renders synchronously and blocks the entire tree until the suspended component resolves. With Concurrent Mode, React can keep doing other work while waiting, it renders an alternative tree in the background and only shows it once it is ready. That prevents loading bars from flickering when data arrives very quickly.

The combination of useTransition and Suspense is especially powerful for page navigation: if a transition update causes a component to suspend, React keeps the current page visible and reports isPending as true until the new content is ready, no layout flash, no skeleton flickering between two pieces of real content. Only once rendering of the new content is complete does the switch happen. This pattern is called "concurrent rendering with Suspense" and is the foundation for React Router v6.4+ and Next.js App Router streaming.

8. Common Mistakes with Concurrent Mode

The most common mistake when migrating to Concurrent Mode is side effects in the render phase. If a component mutates external variables, triggers network requests, or runs console.log calls with side effects during rendering, this leads to multiple unexpected executions, because the render phase can repeat. React StrictMode in the development environment intentionally doubles render calls to make exactly these problems visible. In production this only happens when React actually interrupts and restarts.

A second common mistake: external stores that React doesn't know about. If a component reads an external store and that store changes between the parent's render and the child's render, parent and child see inconsistent data, a so-called tearing bug. The solution is useSyncExternalStore, which tells React how to subscribe to the store and read it consistently. Libraries like Zustand, Jotai and Redux Toolkit have already implemented this internally.


import { useSyncExternalStore } from 'react';

// Correct pattern: useSyncExternalStore prevents tearing with external stores
function useWindowWidth() {
  return useSyncExternalStore(
    // subscribe: React calls this to register a change listener
    (callback) => {
      window.addEventListener('resize', callback);
      return () => window.removeEventListener('resize', callback);
    },
    // getSnapshot: must return same value if nothing changed (stable reference)
    () => window.innerWidth,
    // getServerSnapshot: for SSR environments
    () => 1024
  );
}

// Anti-pattern: reading mutable external variable in render (causes tearing)
let externalState = { count: 0 }; // shared mutable state

function BrokenComponent() {
  // WRONG: externalState may change between parent and child render
  return <div>{externalState.count}</div>;
}

// StrictMode doubles render calls in development to expose side effects
// If your component logs twice, it has unintentional side effects in render

9. Legacy vs. Concurrent Mode Compared

The differences between legacy mode (ReactDOM.render) and Concurrent Mode (createRoot) affect not only internal implementation details but observable behavior across several dimensions. When deciding whether a migration is worthwhile, concrete comparisons of the affected scenarios help.

Aspect Legacy Mode (ReactDOM.render) Concurrent Mode (createRoot) Impact
Rendering Synchronous, not interruptible Interleaved, interruptible No blocking of the main thread
Batching Only in event handlers Automatic everywhere Fewer unnecessary re-renders
useTransition Not available Fully supported Responsiveness under expensive updates
Suspense Code splitting only, no data Data + lazy + streaming Seamless loading transitions
StrictMode Effects once Effects doubled (dev) Render-phase side effects made visible

Migrating from legacy to Concurrent Mode usually requires three steps: replace ReactDOM.render with createRoot, switch external stores over to useSyncExternalStore, and identify render-phase side effects and move them into useEffect. Libraries that haven't yet been checked for Concurrent Mode compatibility can show up through tearing issues, it's worth checking the changelog of the state management library in use before migrating.

Mironsoft

React performance, Concurrent Mode migration and frontend architecture

Migrating your React application to Concurrent Mode?

We analyze your React codebase for Concurrent Mode compatibility, identify tearing risks, and guide the migration from legacy mode to createRoot, including a review of all external state management libraries.

Compatibility Audit

Checking render-phase side effects, external stores and Suspense boundaries

Migration

Introducing createRoot, useSyncExternalStore and useTransition step by step

Performance Measurement

Evaluating React DevTools Profiler and interaction tracing before/after migration

10. Summary

React Fiber is the foundation on which every modern React feature is built. By splitting work into interruptible Fiber nodes and using a lanes-based priority system, the reconciler lets React prioritize urgent updates without blocking non-urgent ones. Concurrent Mode via createRoot unlocks this system for the entire tree. Automatic Batching reduces re-renders everywhere, not just in event handlers. useTransition and useDeferredValue give developers direct control over the prioritization of UI updates.

Migration pays off above all for applications with expensive render operations, long lists, or frequent state updates driven by user input. Anyone using an external store must check whether it supports useSyncExternalStore to avoid tearing. React StrictMode is especially valuable in Concurrent Mode projects: it exposes render-phase side effects through double execution before they turn into hard-to-debug problems in production.

React Fiber & Concurrent Mode: The Essentials at a Glance

Fiber Architecture

Work split into interruptible Fiber nodes. Render phase repeatable, commit phase atomic. Lanes order updates by urgency.

Enabling createRoot

Concurrent Mode applies to the whole tree. Automatic Batching everywhere. flushSync for synchronous flushing when needed.

useTransition & Deferred

Separate urgent from non-urgent updates. isPending for a loading state without extra state. useDeferredValue as an alternative.

Avoiding Tearing

Integrate external stores with useSyncExternalStore. StrictMode's doubled renders reveal render-phase side effects early.

11. FAQ: React Fiber, Concurrent Mode and Priorities

1What is the difference between React Fiber and Concurrent Mode?
Fiber is the internal reconciler architecture since React 16. Concurrent Mode is the rendering mode that builds on Fiber and is enabled via createRoot since React 18. Fiber enables interruptible rendering; Concurrent Mode makes it usable.
2Do I need to rewrite my code completely?
Usually not. createRoot instead of ReactDOM.render, switch external stores to useSyncExternalStore, move render-phase side effects into useEffect. StrictMode shows where adjustments are needed.
3What is tearing in React?
Inconsistent data from an external store in different parts of the UI, because React can re-read the store during rendering. useSyncExternalStore prevents this through snapshot semantics.
4useTransition vs. useDeferredValue?
useTransition when you control the state setter yourself. useDeferredValue when you want to defer a value coming from outside (prop, URL parameter) without access to the setter.
5Why does useEffect run twice in StrictMode?
StrictMode intentionally mounts/unmounts/remounts to check that cleanup functions are implemented correctly. This does not happen in production. Doubled effects mean missing cleanup logic.
6What are Lanes in React?
Bitmasks used to classify updates by urgency. Transparent to developers, controlled through useTransition and startTransition. Higher lanes are processed before lower ones.
7Can I introduce Concurrent Mode incrementally?
Yes, introduce createRoot in a single sub-app while the rest stays in legacy mode. Allows incremental testing without a full switch.
8What is automatic batching in React 18?
Multiple setState calls in event handlers, setTimeout, promises and native listeners are bundled into a single re-render. In React 17 only inside synthetic React event handlers.
9How do I analyze long render times?
React DevTools Profiler: start a recording, perform an interaction, analyze the flame graph. Red bars mean long render times. Concurrent Mode additionally shows interrupted renders.
10Does Suspense work for data without a framework?
Yes, with a promise-throwing cache pattern. React 19 formalizes this with use(). In practice, SWR, React Query or the Next.js App Router are recommended, as they implement Suspense correctly.