Fiber & Concurrent Rendering
โฑ๏ธ ~4-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ณ A restaurant kitchen with priority lanes โ urgent orders jump the queue, prep work pauses for VIP tables.
The two-phase model
React rendering is split into two fundamentally different phases. The render phase is pure โ it builds a description of what the DOM should look like. It can be interrupted, thrown away, and restarted as many times as needed. The commit phase applies the changes to the real DOM. It runs to completion without interruption because a half-applied DOM update would leave the UI in an inconsistent visible state.
Render phase (interruptible)
- โข Calls your component functions
- โข Diffs the fiber tree
- โข Computes what changed
- โข Can be paused, resumed, discarded
- โข MUST be side-effect free
Commit phase (always synchronous)
- โข Applies DOM mutations
- โข Runs useLayoutEffect
- โข Updates refs
- โข Runs useEffect (async)
- โข Always runs to completion
Priority lanes (React 18)
React 18 introduced a lane-based priority system with 16 lanes. Higher-priority lanes preempt lower ones. Common lanes:
The Fiber data structure
| 1 | // Simplified Fiber node (~30 real fields) |
| 2 | interface Fiber { |
| 3 | type: Function | string; // component fn or 'div' |
| 4 | stateNode: Element | null; // real DOM node or class instance |
| 5 | child: Fiber | null; // first child |
| 6 | sibling: Fiber | null; // next sibling |
| 7 | return: Fiber | null; // parent fiber |
| 8 | pendingProps: Props; |
| 9 | memoizedProps: Props; // props from last render |
| 10 | memoizedState: Hook | null;// hook linked list |
| 11 | lanes: Lanes; // pending work priority |
| 12 | flags: Flags; // what changed (Placement, Update, etc.) |
| 13 | alternate: Fiber | null; // the "other" tree (double buffering) |
| 14 | } |
React maintains two trees: the current tree (what's in the DOM) and the work-in-progress tree being built. On commit, the trees swap. This is called double buffering โ the same technique graphics cards use.
Interactive Sandbox
โ Move something, see it react instantly.Rendering strategy
Legacy synchronous rendering โ entire tree processed in one uninterruptible blocking call. Browser cannot run input handlers, animations, or any other work until React finishes.
Work loop timeline
render starts โ browser is now blocked
Challenge
Explore all 5 rendering patterns. Notice the difference between render phases (pausable) and commit phases (always synchronous). Why can render be interrupted but commit cannot?
Why Should I Care?
โ The exact interview question + the bug it kills.Exact interview questions
Q: Why can render be interrupted but commit cannot?
Render is pure โ it only builds a virtual description. If interrupted, React can discard the partial work and restart without side effects visible to users. Commit applies DOM mutations which are immediately visible. A partial commit would show inconsistent UI โ e.g., a button updated but its parent not yet. React runs commit synchronously to guarantee the DOM is always in a consistent state after each update.
Q: What does startTransition do exactly?
| 1 | import { startTransition, useTransition } from 'react'; |
| 2 | ย |
| 3 | // Method 1: startTransition wrapper |
| 4 | startTransition(() => { |
| 5 | setSearchResults(filter(data, query)); // non-urgent |
| 6 | }); |
| 7 | ย |
| 8 | // Method 2: useTransition hook |
| 9 | const [isPending, startTransition] = useTransition(); |
| 10 | <button onClick={() => startTransition(() => setTab('heavy'))}> |
| 11 | {isPending ? <Spinner /> : 'Heavy Tab'} |
| 12 | </button> |
startTransition marks updates inside it as "transition" priority (lower than user input). React can interrupt them if something more urgent arrives. The key benefit: the current UI stays interactive while the transition is computing.
Q: What is "tearing" in Concurrent Mode?
Tearing occurs when different components read different values from the same external store during a single render pass. In Concurrent Mode, renders can be interleaved โ a store update can happen between two component renders in the same tree, causing visual inconsistency. Use useSyncExternalStore (React 18) when integrating external stores like Redux or Zustand to prevent this.
The Deep Dive
โ Spec refs, engine internals, the minutiae.The work loop algorithm
| 1 | // Simplified React work loop (packages/react-reconciler) |
| 2 | function workLoopConcurrent() { |
| 3 | while (workInProgress !== null && !shouldYield()) { |
| 4 | performUnitOfWork(workInProgress); |
| 5 | } |
| 6 | } |
| 7 | ย |
| 8 | function shouldYield(): boolean { |
| 9 | // Scheduler checks remaining time in current 5ms frame |
| 10 | return getCurrentTime() >= deadline; |
| 11 | } |
| 12 | ย |
| 13 | function performUnitOfWork(fiber: Fiber) { |
| 14 | // 1. "Begin" phase: call component, reconcile children |
| 15 | const next = beginWork(fiber); |
| 16 | if (next === null) { |
| 17 | // no children โ "complete" this fiber, walk up |
| 18 | completeUnitOfWork(fiber); |
| 19 | } else { |
| 20 | workInProgress = next; // descend to first child |
| 21 | } |
| 22 | } |
Scheduler package โ time-slicing
React's scheduler uses MessageChannel (not setTimeout) to schedule work after the current task. MessageChannel fires in the macrotask queue but before paint, giving React a chance to do work every ~5ms without blocking the browser's rendering pipeline.
| 1 | // Scheduler internals (simplified) |
| 2 | const channel = new MessageChannel(); |
| 3 | channel.port2.onmessage = performWorkUntilDeadline; |
| 4 | ย |
| 5 | function scheduleWork(callback) { |
| 6 | channel.port1.postMessage(null); // yields to browser, then resumes |
| 7 | } |
| 8 | ย |
| 9 | function performWorkUntilDeadline() { |
| 10 | deadline = getCurrentTime() + 5; // 5ms time slice |
| 11 | workLoop(); // runs until deadline or work exhausted |
| 12 | } |
Streaming SSR + Selective Hydration (React 18)
With renderToPipeableStream, React can stream HTML chunks to the browser as they're ready. When the client receives HTML, it can start hydrating high-priority components (e.g., the nav) before the rest of the page arrives. User interactions during hydration are handled via event capture โ React replays them once the relevant component hydrates.
| 1 | // Server: streaming with Suspense |
| 2 | import { renderToPipeableStream } from 'react-dom/server'; |
| 3 | ย |
| 4 | const { pipe } = renderToPipeableStream(<App />, { |
| 5 | bootstrapScripts: ['/main.js'], |
| 6 | onShellReady() { |
| 7 | res.setHeader('content-type', 'text/html'); |
| 8 | pipe(res); // stream HTML as chunks resolve |
| 9 | }, |
| 10 | }); |
| 11 | ย |
| 12 | // Client: selective hydration |
| 13 | import { hydrateRoot } from 'react-dom/client'; |
| 14 | hydrateRoot(document, <App />); |
| 15 | // React hydrates high-priority components first |
Interview Questions
โ Real questions from real interviews โ with answers.A Fiber is a plain JS work unit representing one component; the old reconciler used the call stack and couldn't be paused.
Render is pure and discardable; commit writes real DOM mutations that are immediately visible to users.
It marks state updates as non-urgent so React can interrupt them if higher-priority work (user input) arrives.
Time-slicing breaks render work into ~5ms chunks yielded to the browser via MessageChannel.
React keeps two fiber trees โ the current (committed) tree and the work-in-progress tree being built โ and swaps them atomically on commit.
Lanes are bit-mask priorities; SyncLane = user input (highest), TransitionLane = startTransition work, IdleLane = background prefetch (lowest).
Memory Game
โ Quick quiz โ lock the concept in long-term memory.How does React's double-buffering differ from how V8 garbage collects objects?