What Are React Fiber and Concurrent Mode?

Advanced14 min interview
Skills tested:
Explaining what Fiber changes structurally: linked list of work units instead of recursive call stackExplaining the current vs work-in-progress tree model and why the commit phase stays synchronousUnderstanding that concurrent features require createRoot, not ReactDOM.renderUsing useTransition to separate urgent from non-urgent state updatesKnowing that startTransition does not make JavaScript off-thread and what to use instead

Advertisement

🧩 Scenario

Fiber and Concurrent Mode are a senior-level React topic. Interviewers test whether you understand the structural change (linked list vs call stack), the two-tree model, and the critical nuance that startTransition is not a Web Worker.

Architecture Walkthrough

Why React Needed a New Reconciler

Before React 16, the reconciler was a recursive, synchronous call stack. When React started diffing a component tree, it had to walk every node before returning control to the browser. On large trees this could hold the main thread for 50-100ms, long enough to drop multiple animation frames and make the app feel unresponsive to input. There was no mechanism to pause, no way to prioritize a keystroke over an ongoing list render. Developers worked around it with virtualization or manual tree splitting, both of which addressed symptoms rather than the scheduling problem.

What Fiber Actually Is

Fiber is React's reimplementation of the reconciler as a linked list of work units rather than a call stack. Each fiber node represents one React element and carries pointers to its parent, child, and sibling. This structure lets React pause after processing any individual fiber and yield back to the browser event loop, then resume from exactly where it stopped. The browser schedules work in tasks on the main thread, and Fiber breaks reconciliation into many small tasks instead of one large one, so the browser can handle input events between them.

Fiber tracks two versions of the tree simultaneously: the "current" tree visible to the user, and the "work-in-progress" tree React is building. When the work-in-progress tree is fully complete, React commits it to the DOM in a single synchronous phase. The commit phase must remain synchronous because React cannot partially update the DOM: a half-rendered tree would show an inconsistent UI. Only reconciliation (computing what changed) is interruptible; actually applying the changes is not.

What Concurrent Features Add on Top

Fiber's architecture makes interruptibility possible, but concurrent features (enabled via createRoot in React 18) are what expose this capability to application code. Without createRoot, React still uses Fiber internally but commits every render synchronously. With createRoot, React assigns priorities to updates: urgent updates (typing, clicking) render immediately; non-urgent updates wrapped in startTransition are marked as interruptible. If a more urgent update arrives while a transition is in progress, React throws away the in-progress work, handles the urgent update, and restarts the transition. isPending communicates to the UI that background rendering is happening.


Key Code Explained

import { createRoot } from 'react-dom/client';
import { useTransition, useDeferredValue, useState } from 'react';

// Step 1: use createRoot to enable concurrent features
const root = createRoot(document.getElementById('root')!);
root.render(<App />);

// Without createRoot (legacy ReactDOM.render), concurrent features are not available
// even in React 18


// useTransition: separate urgent from non-urgent updates
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<Result[]>([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    // Urgent: update the input immediately so typing feels instant
    setQuery(e.target.value);

    // Non-urgent: mark the results update as interruptible
    // If the user types again before results finish rendering,
    // React abandons this render and restarts with the new query
    startTransition(() => {
      setResults(searchLocalIndex(e.target.value));
    });
  };

  return (
    <>
      <input value={query} onChange={handleSearch} />
      {/* isPending is true while the transition render is in progress */}
      {isPending ? <Spinner /> : <ResultList results={results} />}
    </>
  );
}


// useDeferredValue: defer a specific value rather than a callback
// Useful when you don't control the update trigger
function FilteredList({ items, query }: { items: Item[]; query: string }) {
  // deferredQuery lags behind query — React renders with the old value
  // while computing the new one in the background
  const deferredQuery = useDeferredValue(query);

  const filteredItems = useMemo(
    () => items.filter((item) => item.name.includes(deferredQuery)),
    [items, deferredQuery]
  );

  return <ul>{filteredItems.map((item) => <li key={item.id}>{item.name}</li>)}</ul>;
}


// Critical nuance: startTransition does NOT move JavaScript off-thread
startTransition(() => {
  // If this computation takes 200ms, it still blocks the main thread for 200ms
  // startTransition only tells React the resulting STATE UPDATE is low priority
  // It makes the RE-RENDER of components interruptible, not the computation
  const result = expensiveSearch(query); // still runs synchronously
  setResults(result);
});

// For genuinely expensive computation, use a Web Worker
const worker = new Worker(new URL('./search.worker.ts', import.meta.url));
worker.postMessage({ query });
worker.onmessage = (e) => {
  startTransition(() => setResults(e.data)); // now both the compute and render are non-blocking
});

The two-line split in handleSearch is the essence of the pattern: setQuery outside startTransition (urgent, renders synchronously) and setResults inside (non-urgent, interruptible). The user sees their typed character immediately in the input because setQuery is urgent. The result list can lag behind if the user types fast, and isPending shows a non-blocking indicator during the lag.


Tradeoffs

ApproachFixes whatDoes not fixWhen to use
startTransitionSlow React rendering of large listsExpensive JavaScript computationLarge list renders that block input
useDeferredValueSame as above, value-basedSameWhen you do not control the update trigger
useMemoExpensive re-computationSlow renderingExpensive derived values
Web WorkerExpensive JavaScript computationReact renderingCPU-bound work (search indexing, image processing)
VirtualizationLarge DOM treesExpensive per-item rendersLists with thousands of items

What Interviewers Actually Check

  • Whether you can explain the structural change from call stack to linked list and why it enables interruption
  • Whether you know the two-tree model and why the commit phase is synchronous
  • Whether you know createRoot is required to enable concurrent features
  • Whether you know startTransition does not move work off the main thread
  • Whether you can identify when to use startTransition vs useDeferredValue vs a Web Worker

Follow-Up Questions

  1. useDeferredValue and startTransition both defer work. What is the practical difference, and when would you choose one over the other?
  2. The commit phase in Fiber is synchronous even though reconciliation is interruptible. Why can't React make the commit phase interruptible too?
  3. If you have a component that takes 80ms to render even with memoization, does startTransition help? What does?
  4. React 18 introduced automatic batching. How does it interact with transitions?
  5. Your PM says the app feels slow loading a dashboard with 20 heavy chart components. You add startTransition around the data fetch trigger but nothing improves. How would you diagnose what is happening?

Common Candidate Mistakes

  • Saying startTransition moves work to a background thread — it does not; it only lowers render priority
  • Not knowing createRoot is required to enable concurrent features (using ReactDOM.render does nothing)
  • Not knowing the commit phase must be synchronous and not being able to explain why
  • Confusing useDeferredValue (defers a value) with useTransition (defers updates in a callback)
  • Thinking concurrent features fix slow JavaScript computations rather than slow React rendering

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you explain what Fiber changes about the reconciler and why a call stack was the problem?
  • Can you explain the current vs work-in-progress tree model?
  • Can you use useTransition to separate urgent from non-urgent state updates?
  • Can you explain what startTransition does and does not do?
  • Can you explain when to reach for a Web Worker instead of concurrent features?

Summary

React Fiber replaces the old synchronous recursive reconciler with a linked list of interruptible work units, allowing React to pause rendering between individual fiber nodes and yield to higher-priority work. Concurrent features, enabled by createRoot in React 18, expose this scheduling to application code: useTransition marks state updates as low priority so React can abandon their in-progress render if a more urgent update arrives; useDeferredValue applies the same deferral to a specific value. The commit phase remains synchronous because React cannot partially apply DOM updates. The most important nuance is that startTransition only makes React's rendering interruptible — it does nothing for expensive JavaScript computations inside the callback, which still block the main thread. Those require useMemo to reduce recomputation or a Web Worker to move the work off the main thread entirely.

Frequently Asked Questions

Does startTransition move code off the main thread?

No. startTransition still runs on the main thread. What it does is mark the resulting state update as low priority, so React can interrupt the re-render triggered by that update if a more urgent update arrives. Expensive JavaScript computations inside the callback still block the thread. For off-thread computation, use a Web Worker.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement