Topic 26.1
React Internals: Fiber, Lanes and the Scheduler
In one line
Inside React every component instance is a fiber, a plain object linked into a tree; a work loop renders fibers one unit at a time, lanes decide which updates go first, and a finished tree is swapped in during a commit that cannot be interrupted.
Think of it like this
A restaurant kitchen. Each order ticket is a fiber: it says what dish it is, which table it belongs to and what is already done. The head chef works through tickets one at a time and can stop between tickets to handle an urgent one (a lane with higher priority). Dishes are plated on a spare counter first (the work-in-progress tree) and only carried out to the table all at once when the whole order is ready (the commit). Guests never see a half-plated meal.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Fiber
- React's private object for one component instance, DOM element or text node. It remembers props, state, effects and links to its parent, child and sibling.
- Hook list
- A chain of small objects on a function component's fiber, one per hook call, in the order the hooks were called.
- Work loop
- The loop that processes fibers one by one:
beginWorkon the way down,completeWorkon the way up. - Render phase
- React calling components and building a new fiber tree in memory. Can be paused or thrown away.
- Commit phase
- React applying the new tree to the DOM and running refs and effects. Never interrupted.
- Double buffering
- Keeping a current tree (on screen) and a work-in-progress tree (being built), then swapping them.
- alternate
- The field that links a fiber to its twin in the other tree.
- Lane
- One bit that stands for one priority level. Updates are tagged with a lane; higher-priority lanes render first.
- Scheduler
- The package that runs React's work in short slices and gives the browser time to paint and handle input.
- Bailout
- React skipping a fiber (or a whole subtree) because nothing in it has pending work.
Step by step
01From elements to fibers
Your component returns elements: short-lived objects like { type: Item, key: "a", props: {...} }. React turns each one into a fiber the first time it appears, and on later renders it updates the existing fiber at that position instead of creating a new one (as long as the type and key match). That reuse is how state survives re-renders (Topic 6.4).
02The tree is linked with child, sibling and return
A parent points only at its first child. Each child points at the next one with sibling, and every fiber points back to its parent with return. The loop can stop after any fiber and later continue from exactly that fiber.
03beginWork on the way down, completeWork on the way up
The work loop is a depth-first walk. Going down, beginWork renders the fiber and reconciles its children. When a fiber has no child, completeWork finishes it and React moves to its sibling, or climbs to the parent and completes that.
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(fiber) {
const next = beginWork(fiber.alternate, fiber, renderLanes); // call the component, diff children
fiber.memoizedProps = fiber.pendingProps;
if (next !== null) {
workInProgress = next; // go down to the first child
} else {
completeUnitOfWork(fiber); // completeWork, then sibling, else climb via return
}
}04Render can stop, commit can't
The render phase may pause between fibers (time slicing) or be thrown away when a more urgent update arrives. The commit phase runs start to finish in one go, so the user never sees a half-updated page.
05Two trees take turns
After a commit the new tree becomes current. The old fibers are kept as alternates and recycled for the next render. The second live example below shows the same two fiber objects swapping on every click.
06Lanes order the work
Each update carries a lane. The root keeps a bitmask of pending lanes; React picks the highest-priority group, renders it, commits, then looks again. Fibers keep lanes (their own pending work) and childLanes (work somewhere below), so beginWork can bail out of whole subtrees that have nothing to do.
Try it yourself
- 1
Predict the hook list
In the fiber peek, add
const [open] = useState(false);between theuseRefand the seconduseState. Predict the logged hook list before running. (It becomes[ 3, { current: 'hello' }, false, 'Asha' ]: one entry per hook, in call order.) - 2
Remove the transition
In the lanes example, replace
startTransition(() => setSlow((s) => s + 1));with a plainsetSlow((s) => s + 1);. Predict how many renders the click causes now. (One: both updates share the sync lane, so they are batched together, Topic 4.5.) - 3
Make the ref stable
In the order example, move the ref callback out of the component (
const logRef = (node) => ...at the top level) and useref={logRef}. Click again: theref detached (null)and secondref attachedlines disappear, because the ref didn't change.
Code & diagrams
React DOM stores a pointer to the fiber on each DOM node under a key starting with `__reactFiber$`. Walking `return`, `child` and `sibling` shows the linked tree; the List fiber's `memoizedState` is its hook list in call order. These are private and change between versions.
Preview
Console
Nothing logged yet.Expected console output
return chain: ul -> List -> main -> App -> HostRoot
ul.child then .sibling: Item key=a, Item key=b
List hook list: [ 3, { current: 'hello' }, 'Asha' ]
ul fiber's stateNode is the real <ul>: true
List fiber's stateNode: nullOn the first render there is no alternate. Each update builds the work-in-progress Counter from the spare object, the commit flips `root.current`, and the two objects swap roles: fiber#1, fiber#2, fiber#1…
Preview
Console
Nothing logged yet.Expected console output
first render: current = fiber#1 (count 0), alternate = none
after click 1: current = fiber#2 (count 1), alternate = fiber#1
after click 2: current = fiber#1 (count 2), alternate = fiber#2
after click 3: current = fiber#2 (count 3), alternate = fiber#1Render goes parent → child. The commit then runs effects child → parent (children complete first). On the update, the inline ref callback is a new function, so React detaches the old one with `null` and attaches the new one. Layout cleanups run in the mutation step; passive cleanups run just before the new passive effects.
Preview
Console
Nothing logged yet.Expected console output
render Parent 0
render Child 0
insertion effect Child
ref attached
layout effect Child
layout effect Parent
passive effect Child
passive effect Parent
--- click ---
render Parent 1
render Child 1
ref detached (null)
insertion effect Child
layout cleanup Child
ref attached
layout effect Child
layout effect Parent
passive cleanup Child
passive effect Child
passive effect ParentBoth updates come from the same click, but they land in different lanes. React renders and commits the sync lane first (slow is still 0 in that render), then renders the transition lane. The order in your code doesn't matter: priority does.
Preview
Console
Nothing logged yet.Expected console output
render: urgent = 0 slow = 0
--- click ---
render: urgent = 1 slow = 0
render: urgent = 1 slow = 1Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Change the hook order between renders
In the double-buffering example, add if (count > 0) { const saved = useRef(count); } right after the useState line (import useRef too). The first render calls one hook, the next render calls two.
Myth vs fact
Myth
Fiber is a new virtual DOM.
Fact
The element tree is the "virtual DOM" description. Fiber is the persistent tree of work units React keeps alongside it: it holds state, effects and priorities and makes rendering interruptible.
Myth
Concurrent rendering means React renders on several threads.
Fact
Everything runs on the main thread. Concurrent means React can interleave: pause a render, let the browser handle input, and resume or restart it.
Myth
useEffect runs right after the DOM changes.
Fact
useLayoutEffect runs right after the DOM changes, before paint. useEffect runs later, usually after the browser has painted.
Myth
React compares the whole old and new DOM on every update.
Fact
React compares new elements with existing fibers, and only for fibers that have work. lanes and childLanes let it skip untouched subtrees entirely.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Field and function names here are from React 19's implementation (
workLoopConcurrentByScheduler,beginWork,completeWork,commitMutationEffects,flushPassiveEffects). They are not public API and change between versions; interviewers care about the ideas, not exact names. - ▸
Because the render phase can be repeated or abandoned, anything with side effects in render (mutating a module variable, starting a fetch, logging analytics) can run more times than you expect. StrictMode's double render (Topic 7.4) is a cheap simulation of exactly that.
- ▸
flushSync(Topic 8.6) forces an update intoSyncLaneand commits it before returning, which is why it hurts performance if overused: it skips time slicing and batching for that update. - ▸
In React 19's implementation a transition that keeps getting interrupted is not starved forever: after about 5 seconds its lane is marked expired and React renders it without yielding. This is also why a very slow transition can eventually make the page feel frozen: split the work or virtualise (Topic 16.9).
Remember this
- 1
A fiber is a plain JavaScript object that React keeps for every component instance, every DOM element and every text node in your app. It is React's long-lived memory between renders: the element objects your components return are thrown away each render (Topic 1.7), but fibers stay. Important fields:
tag(what kind of fiber: function component, class component, host element, text, root…),type(your function, your class, or a tag name like"ul"),key(from Topic 3.3),stateNode(the real DOM node for a host element, the instance for a class component,nullfor a function component),memoizedProps(the props used last time),memoizedState,flags(what must happen to this fiber in the commit: place it, update it, run its effects) andlanes(which priorities of pending work it has). - 2
Fibers form a tree through three pointers instead of a children array:
child(the first child),sibling(the next child of the same parent) andreturn(the parent; it is called *return* because that is where the work goes back to when this fiber is done). This linked shape lets React walk the tree with a simple loop and pause anywhere, which a recursive function cannot do. For a function component,memoizedStateholds the hook list: one object per hook call, in call order, linked withnext. That is why hooks must be called in the same order every render (Topic 4.3): React matches the firstuseStatecall to the first object in the list, and so on. - 3
The work loop renders the tree one fiber at a time.
performUnitOfWorkcalls **beginWorkon a fiber: for a function component that means calling your function with its props and hooks, then reconciling the children it returned against the existing child fibers (the diffing rules of Topic 16.2). If the fiber has a child, that child is next. If not,completeWork** runs: for a host element it creates the DOM node on first mount (not yet attached to the page) or works out which attributes changed, and it bubbles the fiber'sflagsup to the parent'ssubtreeFlags. Then the loop moves to the sibling, or back up viareturn. It is a depth-first walk you could write yourself. - 4
React works in two phases. The render phase (all the
beginWork/completeWorkabove) only calls your components and builds a new tree in memory. It may be paused, restarted or thrown away, so render code must be pure (Topic 2.6). The commit phase applies the result and always runs to the end without stopping, in sub-steps: before mutation (read the DOM before it changes, e.g.getSnapshotBeforeUpdate), mutation (insert, update and remove DOM nodes, detach old refs, runuseInsertionEffect, run layout-effect cleanups), then the tree swap, then layout (attach refs, runuseLayoutEffectandcomponentDidMount/Update, before the browser paints) and finally passive effects (useEffect, usually after the paint). Topic 4.8 introduced trigger → render → commit; this is the zoomed-in view. - 5
Double buffering means React keeps two versions of each fiber. The current tree matches what is on screen. Renders build a work-in-progress tree, and each work-in-progress fiber points to its current twin through
alternate. When the commit finishes, the root'scurrentpointer flips to the new tree and the old one becomes the spare. Next time, React reuses those spare objects instead of allocating new ones, so the two fiber objects take turns, like two plates rotating between the kitchen and the table. It also means a render that gets thrown away never touched what is on screen. - 6
Lanes are React's priority system. A lane is one bit in a 31-bit number, so a set of pending priorities is a bitmask that React can merge and test with
|and&. When you call a setter, React picks a lane from where the update came from: a discrete event such as a click or key press getsSyncLane, continuous input likemousemoveor scroll getsInputContinuousLane, updates from timers or network callbacks getDefaultLane,startTransitionand the background render ofuseDeferredValueget transition lanes (Topic 16.7), Suspense retries get retry lanes, and hidden<Activity>content getsOffscreenLane(Topic 16.10). React always renders the highest-priority lanes first. In React 19's implementation, blocking lanes expire after about 250 ms and default or transition lanes after about 5 seconds; an expired lane is rendered without yielding so it can't starve. - 7
The scheduler (the separate
schedulerpackage) decides *when* React works. Sync lanes render withworkLoopSync, which never stops. Other lanes render with a loop that asksshouldYield()between fibers; in React 19's implementation the scheduler yields after about 5 ms and continues in a new macrotask (viaMessageChannel), so the browser can paint and handle input in between. This time slicing is what lets typing stay responsive while a big transition renders, and why an urgent update can interrupt a transition: React drops the half-built work-in-progress tree, renders the urgent update, then starts the transition again.
Explain it without notes
What is a fiber and what does it store?
Walk through the render phase and the commit phase. Which one can be interrupted, and why does that matter for your code?
What is double buffering in React?
What are lanes, and how does React decide which update renders first?
How does the scheduler keep the page responsive during a big render?
Practice
Write a function countFibers(fiber) that walks a fiber subtree using only child, sibling and return (no recursion) and returns how many fibers it has.
Predict the console order for a component A with a useLayoutEffect and a useEffect, rendering a child B with the same two effects, on first mount.
Trade-offs
- ↔
Interruptible rendering keeps input responsive, but it makes render purity mandatory and means a component may render more times than it commits.
- ↔
Fibers and double buffering cost memory (two objects per node) in exchange for cheap restarts and fewer allocations.
Done when you can
I can name the main fields of a fiber and what each is for.
I can describe the work loop with
beginWorkandcompleteWork.I can list the commit sub-phases and where each kind of effect runs.
I can explain double buffering and
alternate.I can explain lanes and how an urgent update overtakes a transition.
I can explain time slicing and why render code must be pure.