Command Palette

Search for a command to run...

Hectal
PHASE 26Advanced ~28 min· topic 1 of 5

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: beginWork on the way down, completeWork on 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).

From elements to fibersdiagram
Rendering diagram…

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.

The tree is linked with child, sibling and returndiagram
Rendering diagram…

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.

work-loop (simplified from React 19's implementation)whole filejavascript
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.

Render can stop, commit can'tdiagram
Rendering diagram…

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.

Lanes order the workdiagram
Rendering diagram…

Try it yourself

  1. 1

    Predict the hook list

    In the fiber peek, add const [open] = useState(false); between the useRef and the second useState. Predict the logged hook list before running. (It becomes [ 3, { current: 'hello' }, false, 'Asha' ]: one entry per hook, in call order.)

  2. 2

    Remove the transition

    In the lanes example, replace startTransition(() => setSlow((s) => s + 1)); with a plain setSlow((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. 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 use ref={logRef}. Click again: the ref detached (null) and second ref attached lines disappear, because the ref didn't change.

Code & diagrams

Peek at the fiber tree (React's private fields) New tab

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

Press Run to see the page.

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: null
Double buffering: two fiber objects take turns New tab

On 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

Press Run to see the page.

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#1
Render, then the commit sub-phases, in order New tab

Render 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

Press Run to see the page.

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 Parent
Lanes: one click, two renders React 18+ New tab

Both 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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

render: urgent = 0 slow = 0
--- click ---
render: urgent = 1 slow = 0
render: urgent = 1 slow = 1

Break 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.

terminal
$ Console after the first click
── what you'll see ──
React has detected a change in the order of Hooks called by Counter. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://react.dev/link/rules-of-hooks
 
Previous render Next render
------------------------------------------------------
1. useState useState
2. undefined useRef
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
Uncaught Error: Rendered more hooks than during the previous render.

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 into SyncLane and 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. 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, null for 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) and lanes (which priorities of pending work it has).

  2. 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) and return (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, memoizedState holds the hook list: one object per hook call, in call order, linked with next. That is why hooks must be called in the same order every render (Topic 4.3): React matches the first useState call to the first object in the list, and so on.

  3. 3

    The work loop renders the tree one fiber at a time. performUnitOfWork calls **beginWork on 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's flags up to the parent's subtreeFlags. Then the loop moves to the sibling, or back up via return. It is a depth-first walk you could write yourself.

  4. 4

    React works in two phases. The render phase (all the beginWork/completeWork above) 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, run useInsertionEffect, run layout-effect cleanups), then the tree swap, then layout (attach refs, run useLayoutEffect and componentDidMount/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. 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's current pointer 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. 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 gets SyncLane, continuous input like mousemove or scroll gets InputContinuousLane, updates from timers or network callbacks get DefaultLane, startTransition and the background render of useDeferredValue get transition lanes (Topic 16.7), Suspense retries get retry lanes, and hidden <Activity> content gets OffscreenLane (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. 7

    The scheduler (the separate scheduler package) decides *when* React works. Sync lanes render with workLoopSync, which never stops. Other lanes render with a loop that asks shouldYield() between fibers; in React 19's implementation the scheduler yields after about 5 ms and continues in a new macrotask (via MessageChannel), 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

01

What is a fiber and what does it store?

02

Walk through the render phase and the commit phase. Which one can be interrupted, and why does that matter for your code?

03

What is double buffering in React?

04

What are lanes, and how does React decide which update renders first?

05

How does the scheduler keep the page responsive during a big render?

Practice

01

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.

02

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 beginWork and completeWork.

  • 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.