What Are React Fiber and Concurrent Mode?
Advertisement
🧩 Scenario
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
| Approach | Fixes what | Does not fix | When to use |
|---|---|---|---|
startTransition | Slow React rendering of large lists | Expensive JavaScript computation | Large list renders that block input |
useDeferredValue | Same as above, value-based | Same | When you do not control the update trigger |
useMemo | Expensive re-computation | Slow rendering | Expensive derived values |
| Web Worker | Expensive JavaScript computation | React rendering | CPU-bound work (search indexing, image processing) |
| Virtualization | Large DOM trees | Expensive per-item renders | Lists 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
createRootis required to enable concurrent features - Whether you know
startTransitiondoes not move work off the main thread - Whether you can identify when to use
startTransitionvsuseDeferredValuevs a Web Worker
Follow-Up Questions
useDeferredValueandstartTransitionboth defer work. What is the practical difference, and when would you choose one over the other?- The commit phase in Fiber is synchronous even though reconciliation is interruptible. Why can't React make the commit phase interruptible too?
- If you have a component that takes 80ms to render even with memoization, does
startTransitionhelp? What does? - React 18 introduced automatic batching. How does it interact with transitions?
- Your PM says the app feels slow loading a dashboard with 20 heavy chart components. You add
startTransitionaround the data fetch trigger but nothing improves. How would you diagnose what is happening?
Common Candidate Mistakes
- Saying
startTransitionmoves work to a background thread — it does not; it only lowers render priority - Not knowing
createRootis required to enable concurrent features (usingReactDOM.renderdoes nothing) - Not knowing the commit phase must be synchronous and not being able to explain why
- Confusing
useDeferredValue(defers a value) withuseTransition(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
useTransitionto separate urgent from non-urgent state updates? - Can you explain what
startTransitiondoes 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.
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