Suggest an editImprove this articleRefine the answer for “React fiber and virtual DOM update process”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**React Fiber** is React 16's reconciliation engine, rebuilt as a linked list of fiber nodes with `child`, `sibling`, and `return` pointers. ```jsx const [isPending, startTransition] = useTransition(); startTransition(() => setHeavyList(newList)); // low-priority update ``` **Key point:** the render phase is pausable and interruptible; the commit phase is always synchronous and atomic.Shown above the full answer for quick recall.Answer (EN)Image**React Fiber** is React 16's reimplementation of the reconciliation engine as a linked-list data structure that enables incremental, interruptible, and priority-based rendering of the virtual DOM. ## Theory ### TL;DR - Fiber is like an assembly line with pause buttons: old React processed the entire component tree in one synchronous pass (blocking the browser for 100ms+ on large trees); Fiber pauses low-priority work to keep user interactions responsive. - Main difference: stack-based reconciler (pre-16) vs linked-list fiber nodes. The linked list can be paused mid-tree and resumed from the exact same node. - Each fiber node stores three pointers: `child` (first child), `sibling` (next sibling), `return` (parent). - `createRoot` in React 18 enables full concurrent features; `ReactDOM.render` stays on the legacy synchronous path. - Rule of thumb: large lists (500+ items) or animations running alongside data fetches - use `startTransition` or `useDeferredValue`. ### Quick example ```jsx import { useState, useTransition } from 'react'; function App() { const [input, setInput] = useState(''); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { setInput(e.target.value); // high priority: runs immediately startTransition(() => { filterLargeList(e.target.value); // low priority: Fiber schedules in background }); }; return <input value={input} onChange={handleChange} placeholder="Type without lag" />; } ``` `startTransition` tells Fiber this update can wait. The input stays responsive because Fiber yields the heavy work to the browser between 16ms frames, resuming only when the main thread is free. ### Stack reconciler vs Fiber Before React 16, the reconciler walked the component tree using the call stack: one big synchronous operation. A tree with 1,000 nodes at 0.1ms each locked the browser for 100ms. No pausing, no yielding. The user clicked a button and nothing happened until React finished. Fiber replaces the call stack with a linked list. React walks it via `nextUnitOfWork`, processes 5-10ms of work per chunk, then checks if the browser needs to paint or handle an event. If yes, it yields via `shouldYield()` and returns to `nextUnitOfWork` exactly where it stopped. The work-in-progress tree is the `current.alternate` clone, so the committed UI stays intact while React builds the next version. That is the whole architecture change. ### Render phase: building the work-in-progress tree Every state change from `useState` or `setState` enters an update queue attached to the fiber. React reads that queue and starts building a work-in-progress (WIP) tree by cloning the current fiber tree with the new state applied. During this phase, React calls your render functions and runs `reconcileChildFibers` to diff props and state against the last committed fiber. If element types match, it patches the existing fiber node. If a type changes (say, `div` to `span`), React tears down that branch and builds a new one from scratch. Keys matter here. For lists, `key` props let React match old and new nodes by identity rather than position. Without keys, swapping two list items registers as two separate changes instead of zero. The render phase is pure and pausable. No DOM mutations happen here. ### Commit phase: atomic and non-interruptible Once the WIP tree is complete, React enters the commit phase. This part cannot be paused. Three sub-phases run in sequence: - `getSnapshotBeforeUpdate` reads the DOM before any mutations. - `commitMutationEffects` adds, removes, and updates real DOM nodes. - `commitLayoutEffects` fires `componentDidMount`, `componentDidUpdate`, and `useLayoutEffect` synchronously. After the browser paints, `useEffect` runs asynchronously. The commit phase is always synchronous. In practice, this is the thing that catches most developers off guard: rendering 10,000 real DOM nodes in one commit still blocks the browser regardless of concurrent mode. Fiber splits the work of building the WIP tree, not the work of writing to the DOM. That is why virtualization with `react-window` still matters for long lists. ### How Fiber schedules work Fiber does not use `requestIdleCallback` directly because browser support is inconsistent and its 50ms minimum delay is too coarse. Instead, React's `scheduler` package polyfills it with `MessageChannel` `postMessage` loops, slicing work into ~5ms chunks. The `shouldYield()` function checks a deadline; when time is up, the current unit of work pauses and control returns to the browser. Priority is encoded as 32-bit lane bitmasks. `SyncLane` (bit 0) handles user input. Higher bits correspond to lower-priority work: transitions, deferred values, background tasks. When `startTransition` wraps a state update, React ORs `TransitionLane` into the fiber's `lanes` field and propagates it up the tree via `markUpdateLaneFromFiberToRoot`. Higher-priority lanes (lower bit number) preempt lower-priority ones. ### When to use concurrent features Large lists or tables with 500+ items: wrap the heavy state update in `startTransition` to prevent input lag. Animations running alongside expensive re-renders: `useDeferredValue` keeps the animation lane separate from the slow data lane. Simple apps with small component trees: default synchronous mode is fine. Concurrent mode adds scheduling overhead without benefit below roughly 100 nodes. Legacy codebases on `ReactDOM.render`: these run the stack-based path. Migrating to `createRoot` enables concurrent features, but some lifecycle patterns and third-party libraries behave differently. Test thoroughly before switching. ### Comparison table | Feature | Stack reconciler (pre-16) | Fiber (16+) | |---|---|---| | Internal structure | Call stack | Linked list (child / sibling / return) | | Interruptible | No - one atomic pass | Yes - pause and resume per node | | Priority levels | None | 32-bit lane bitmask (5+ levels) | | Scheduling | setTimeout hacks | `scheduler` package with MessageChannel | | Concurrent mode | Not possible | Available via `createRoot` | | When to use | Tiny apps, no animations | Production apps with complex trees | ### Common mistakes **Wrapping everything in `startTransition`.** Transitions defer re-renders, but the state update that drives the input value still needs to run synchronously. Wrapping that update in a transition makes the input itself lag. ```jsx // Wrong: input lags because the value update is deferred startTransition(() => setInputValue(e.target.value)); // Right: defer only the expensive secondary update setInputValue(e.target.value); // immediate startTransition(() => setFiltered(computeFilter(e.target.value))); ``` **Assuming concurrent mode removes all jank.** The commit phase is always synchronous. If 20,000 DOM nodes land in one commit, the browser blocks. Fiber cannot split that. ```jsx // Still jank: 20,000 real DOM nodes committed at once {items.map(item => <div key={item.id}>{item.text}</div>)} // Fix: virtualize <FixedSizeList height={500} itemCount={20000} itemSize={35}> {Row} </FixedSizeList> ``` **Using `flushSync` for every "immediate" update.** `flushSync` forces a synchronous render and bypasses Fiber's scheduling entirely. Used frequently, it causes jank cascades. Reserve it for cases where you must read layout immediately after a state update (focus management, scroll anchoring). **Ignoring StrictMode's double-invoke with async transitions.** In development, StrictMode runs effects twice to surface side effects. A `fetch` inside `useEffect` wrapped in `startTransition` fires twice. Without an `AbortController` or a `cancelled` flag, the first response can overwrite the second. This is expected behavior from StrictMode, not a Fiber bug. ### Real-world usage - React 18 apps: `createRoot` + `startTransition` for responsive typing in large task lists, as seen in tools like Linear. - Next.js 13+ app router: automatic concurrent rendering; server components stream using Fiber lanes to prioritize above-the-fold content. - Redux Toolkit: `useSelector` combined with `useDeferredValue` for admin dashboards with heavy store updates. - TanStack Query: `useSuspenseQuery` integrates with Fiber's bailout mechanism to abort stale queries when a higher-priority update interrupts. ### Follow-up questions **Q:** Draw Fiber's linked-list structure. What are the three pointers on each node? **A:** Each fiber has `child` (first child component), `sibling` (next component at the same level), and `return` (parent). Traversal goes: try `workInProgress.child`; if null, try `workInProgress.sibling`; if null, walk `workInProgress.return` until a sibling is found. The WIP tree is `current.alternate`. **Q:** How does Fiber schedule work without `requestIdleCallback`? **A:** React's `scheduler` package uses `MessageChannel` `postMessage` to get a callback after the browser paints. Inside each callback, `shouldYield()` checks a ~5ms deadline and returns true when time runs out. This gives finer control than `requestIdleCallback`, which has a 50ms minimum idle period. **Q:** What is the difference between the render phase and the commit phase? **A:** Render phase is pure and pausable - it builds the WIP tree, diffs fiber nodes, and collects an effect list. No DOM changes. Commit phase is synchronous and atomic - it applies DOM mutations, then runs lifecycle methods and effects in a fixed order. **Q:** Explain lane bitmasks. Give an example with two priorities. **A:** Lanes are a 32-bit integer. Bit 0 (value 1) is `SyncLane` for user input. Bit 4 (value 16) is `TransitionLane` for `startTransition` updates. React ORs the new lane into the fiber's `lanes` field and runs lower bit numbers first. So user input always preempts a transition in flight. **Q (senior):** Why does `useDeferredValue` not cause an infinite re-render loop even in StrictMode? **A:** `useDeferredValue` schedules a low-priority re-render with the deferred value. On that re-render, the deferred value matches the current value, so React bails out because no state changed. StrictMode double-invokes the render function, but both invocations share the same `current` fiber and bail out identically. No new update is scheduled. **Q (senior):** Why does SSR not use Fiber's concurrent scheduling? **A:** SSR streams HTML synchronously to the client. There is no browser scheduler to yield to - no paint frames, no user events to interrupt for. React uses a simpler synchronous render path on the server. `ReactDOMServer.renderToString` processes the entire tree in one pass. ## Examples ### Basic: seeing Fiber yield in action ```jsx import { useState, useTransition } from 'react'; function SlowList({ count }) { return ( <> {Array.from({ length: count }, (_, i) => ( <div key={i} style={{ padding: '4px', borderBottom: '1px solid #eee' }}> Item {i + 1} </div> ))} </> ); } function App() { const [count, setCount] = useState(100); const [isPending, startTransition] = useTransition(); return ( <div> <button onClick={() => startTransition(() => setCount(c => c + 500))} > Add 500 items {isPending && '(rendering...)'} </button> {/* Fiber renders SlowList in background, yields to button clicks */} <SlowList count={count} /> </div> ); } ``` `isPending` turns `true` while Fiber is still building the WIP tree for the new count. The button stays clickable and responsive throughout the update. Without `startTransition`, clicking Add 500 items would block the UI until the commit finishes. ### Intermediate: `useDeferredValue` for a filtered list ```jsx import { useState, useDeferredValue, memo } from 'react'; const ItemList = memo(({ filter, items }) => { const filtered = items.filter(item => item.label.toLowerCase().includes(filter.toLowerCase()) ); return ( <ul> {filtered.map(item => <li key={item.id}>{item.label}</li>)} </ul> ); }); function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); // low-priority copy const items = useMemo(() => generateItems(1000), []); return ( <> <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Filter items..." /> {/* Re-renders with deferredQuery - always one step behind query */} <ItemList filter={deferredQuery} items={items} /> </> ); } ``` `deferredQuery` lags one or more renders behind `query`. Fiber schedules the `ItemList` re-render at low priority. The input updates on every keystroke without waiting for the list to filter. `memo` prevents `ItemList` from re-rendering when only `query` (not `deferredQuery`) changes. ### Advanced: concurrent fetch in StrictMode with cleanup ```jsx import { useState, useEffect, useTransition } from 'react'; function DataFetcher() { const [data, setData] = useState(null); const [isPending, startTransition] = useTransition(); useEffect(() => { let cancelled = false; // StrictMode double-invokes this effect in development: // mount -> cleanup (cancelled = true) -> mount again // Without the flag, the first response can overwrite the second startTransition(() => { fetch('/api/heavy-data') .then(res => res.json()) .then(json => { if (!cancelled) setData(json); // bail if effect was cleaned up }); }); return () => { cancelled = true; }; }, []); if (isPending) return <p>Loading...</p>; return <pre>{JSON.stringify(data, null, 2)}</pre>; } ``` Fiber's `shouldYield` can also abort the low-priority transition mid-execution if a higher-priority update arrives between the fetch start and its resolution. The `cancelled` flag handles both StrictMode double-invoke and that interruption case. Without it, you get either a stale state write or a React warning about updating an unmounted component.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.