React Fiber

A tutorial on how React Fiber works, read out of React 19.1.0's own source. Eleven small component trees with the work loop walked across them — what setState actually starts, why a recursive render could not be paused, the linked lists that replaced the call stack, how children are matched by key, the two trees swapped at commit, and the half-built tree that gets thrown away when a keystroke arrives — plus a captured profile where the same update is one 83 ms block or sixteen 5 ms ones.

A frontend app built by Ananda Rizki. More of them at Labs.

React Fiber

What happens between setState and the screen changing.

Fiber is what happens between setState and the screen changing.

You already know the cycle you work in: state changes, the component re-renders, the screen catches up. Fiber is the machinery in the middle of that. It is not an API you call and not something you import — it is how React’s insides were rebuilt for React 16, and every version since works this way.

Why rebuild them: the original React walked your component tree with ordinary recursive function calls. A render was therefore a pile of live function calls, and a pile of live function calls cannot be paused — once a render started, the browser did not get the thread back until it ended. On a long list you could feel that: you type, and the letters arrive late. Fiber exists so React can stop a render half-way, hand the browser its thread back, and pick the render up again afterwards.

Eleven lessons follow, one small program each. By the end you should be able to say what a fiber actually is, what render and commit each do, and why “concurrent” does not mean “faster”.

THE WORDS, FIRST

The call stack

The pile of function calls running right now. a() calls b(), so b sits on top of a, and nothing else on the page happens until the pile empties. You cannot pause it half-way — which is the entire problem Fiber was built to solve.

The heap

Ordinary memory, where objects live. An object on the heap stays because something points at it, not because a function is still running — so React can put one down, do something else, and come back to it.

A pointer

One object holding a reference to another. fiber.child is a pointer: follow it and you are standing on the child. Remembering a position is then just keeping one pointer in a variable.

A linked list

A chain where each item names the next one, instead of a numbered array. React chains a fiber's children this way, so from any fiber it can work out where to go next without remembering how it got there.

Render and commit

The two halves of an update, and the distinction the whole page rests on. Render works out what the new tree should look like and changes nothing you can see; it may be paused, thrown away and redone. Commit applies the result to the DOM in one uninterruptible go.

An effect

Work React owes you after it changes the DOM — running your useEffect, attaching a ref. Each fiber carries flags saying what it needs, and commit is where they are paid.

A host component

A plain DOM element — div, span, button — as opposed to a component you wrote. React calls them host components because they belong to the host environment rather than to React, and the diagrams label them that way. A host fiber is the only kind that owns a real DOM node.

A lane

A label on an update saying how urgent it is. A click gets an urgent one; an update inside startTransition gets one React is allowed to interrupt. That label is what decides whether a render can be stopped.

A task

One turn of the browser's event loop. It runs a job to the end, then looks at the queue again — so a click handler cannot start until whatever is running has finished. Everything about interruptibility is about keeping each turn short.

Pure

A function is pure if calling it twice with the same input gives the same result and changes nothing else. React requires rendering to be pure, because a render it throws away and re-runs would otherwise do half its side effects twice.

THE LIFE OF ONE UPDATE

  1. 01

    An update is scheduled

    setState does not render anything. It puts an update on the fiber's queue, marks that fiber and every ancestor with a lane — a priority — and asks the Scheduler for a callback. A click's update and a startTransition update get different lanes, and that difference decides everything that follows.

  2. 02

    Render: build a second tree

    React walks from the root, one fiber at a time: beginWork on the way down works out what a fiber's children are, completeWork on the way back up creates or updates the DOM instance and bubbles effect flags to the parent. All of it lands in a work-in-progress tree. Nothing on screen changes during any of this.

  3. 03

    Yield, if the lane allows it

    For lanes React is allowed to defer, the loop checks a clock between units of work and hands the main thread back about every 5ms, resuming later from the same pointer. For urgent lanes it does not check at all and renders in one pass.

  4. 04

    Commit: apply it all at once

    Synchronous and uninterruptible, in three sub-phases: before-mutation (getSnapshotBeforeUpdate), mutation (DOM insertions, updates and deletions, and layout-effect cleanup), then layout (componentDidMount, componentDidUpdate, useLayoutEffect). The pointer swap — root.current = finishedWork — happens between the mutation and layout phases, so a layout effect already sees the new tree as current.

  5. 05

    Paint, then passive effects

    React calls requestPaint() to ask the Scheduler to get out of the way, and the browser paints. Your useEffect callbacks are flushed separately, after — usually. If the update was on a sync lane, React flushes them at the end of the commit instead, in the same task. useLayoutEffect is the one with a guarantee.

THE PARTS, IN DETAIL

A fiber
A plain JavaScript object, one per element, holding the element's type, its props, its state, the DOM node it owns, effect flags, its lanes, and the three pointers. It is also the unit of work — hence the name.
child, sibling, return
child points at the first child only; the rest hang off it through sibling; return points back at the parent. Children are a linked list, not an array, and return does the job a call stack's return address would.
The work loop
Two loops, differing by one clause. workLoopSync is while (workInProgress !== null); workLoopConcurrentByScheduler adds && !shouldYield(). Which one runs is decided per render by the lane. Because the position in the walk is a pointer rather than a stack of live calls, the second can stop between any two fibers and carry on later.
Hooks
A second linked list, hanging off fiber.memoizedState, one object per hook call, appended in call order. A hook is identified by its position in that chain and by nothing else — no name is ever recorded — which is the whole reason the rules of hooks exist.
Keys
How beginWork tells which of the previous children a new element is. Children are walked in step while the keys line up; the moment they do not, the rest go into a Map keyed by key — or by index, for a child you gave no key. That fallback is what makes an index key quietly wrong on a list that reorders.
current and workInProgress
Two trees, paired fiber by fiber through alternate. One is on screen; the other is being built. Commit is a single pointer swap, and the tree that was replaced becomes the scratch space for the next render.
Reconciliation
The child-diffing that happens inside beginWork — matching new elements against the previous children by type and key to decide what can be reused. This predates Fiber and is a separate idea from it.
Lanes
Priorities as single bits in one 31-bit number, so a whole set of pending updates is one integer. SyncLane is 0b10, for discrete input; there are fourteen separate transition lanes, four retry lanes, and IdleLane for work that can wait indefinitely. A lane that has been starved past its expiry is rendered synchronously regardless.
The Scheduler
A separate package with its own priority queue and no React in it. It runs a callback, and to get a fresh task after yielding it posts itself a message on a MessageChannel port — not requestIdleCallback, whose timing is too conservative for this.
Bailout
Equal props and no update scheduled on a fiber means beginWork clones it rather than calling the component. If its childLanes are empty too, nothing below needs work either and the whole subtree is skipped — never visited. This is what memo and stable props are for.
What time slicing is not
It is not on for everything. React only slices lanes it is allowed to defer, such as transitions; an update from a click renders in a single uninterrupted pass. Concurrent rendering changes the shape of the work on the main thread, not the amount.

WHAT THIS CHANGES IN YOUR CODE

Mark the slow update as non-urgent

startTransition (or useTransition) puts an update on a lane React may interrupt, and useDeferredValue does the same for one value. The classic case is a text input filtering a long list: the keystroke stays urgent, the list re-render becomes interruptible, so typing stops feeling sticky.

Give React a reason to bail out

A fiber is skipped when its props are unchanged and nothing is scheduled on it. memo makes that a shallow comparison instead of an identity one, and useCallback/useMemo keep the props identical between renders so the comparison can succeed. The React Compiler, stable since 2025, writes all of that for you from an analysis of your code — the mechanism is unchanged, only who types it.

Keep render pure, and let StrictMode prove it

A render can be thrown away and run again, so a component that writes to anything outside itself will do it twice. <StrictMode> calls your component body — and the functions you pass to useState, useMemo and useReducer — twice in development for exactly this reason, and runs an extra setup/cleanup cycle on every Effect to find a missing cleanup.

Keep a single component cheap

The loop checks its clock between fibers, never inside one. A component that takes 40ms to render overruns the slice by 40ms and no amount of concurrency can stop it part-way — the smallest unit React can interrupt is one of your components.

WHAT PEOPLE GET WRONG

“Fiber pauses work and resumes wherever it left off.”

Half right. Yielding to the browser keeps the work-in-progress tree and resumes from the same pointer. But a higher-priority update arriving mid-render throws that tree away and starts again from the root — React does not resume a partially built tree at a new priority.

“Concurrent rendering makes React faster.”

It does the same work, plus scheduling overhead, and sometimes does it twice when a render is discarded and restarted. What it buys is a shorter longest block, so clicks and frames can get in. Latency, not throughput.

“useEffect always runs after the browser paints.”

Usually, not always — and this page used to say it too. React defers passive effects and asks the Scheduler to let a paint happen first, but after a commit whose lanes include SyncLane — anything from a click or a keypress — it flushes them at the end of the commit, in the same task. React's own docs say so: an Effect caused by an interaction may run before the browser paints. useLayoutEffect is the one that is guaranteed to run first.

“Hooks are magic, or the compiler tracks them somehow.”

They are a linked list and a counter. useState takes the next object in a chain hanging off the fiber; nothing anywhere records which hook you meant. That is why an early return breaks them, and why React can only tell you the count was wrong.

“A key just silences a warning.”

A key is the only identity a child has. Without one, React files the old children under their index, so reordering a list matches each element against whatever was previously in that position — DOM nodes are reused for different items, and whatever lived in them, from focus to scroll offset, stays behind with the position.

“Fiber is the virtual DOM diffing algorithm.”

The diff is reconciliation, and React had it before Fiber existed. Fiber is the data structure and the scheduling model underneath: a tree of objects on the heap instead of a recursive walk, so the diff became something React could stop in the middle of.

Everything on this page was read out of react@19.1.0 — the files, the function names and the constants — and the numbers in the main-thread view were captured in a browser rather than invented. Sources lists all of it with links. . These are internals: React does not promise them to you and has renamed them before. The shape — a linked tree, a loop, two trees swapped at commit — has been stable since React 16.

The lessons take the same ideas one tree at a time, and you can watch the loop walk them.

Labs — by Ananda Rizki

Simulator

React Fiber

Eleven lessons, each a small component tree with React's work loop walked across it one fiber per frame: beginWork down, completeWork back up, with the code panel showing React's own source and saying, on every lesson, whether what you are reading is verbatim or simplified. It starts before the jargon does — what setState really does (it writes a lane and walks to the root, it does not render), and why the pre-Fiber recursive render could not be paused, because the position in a recursive walk is the JavaScript call stack and a call stack cannot be saved in a variable. From there: the fiber tree as a linked structure, hooks as a second linked list and why that is the real reason for the rules of hooks, the work loop, how keys decide which fiber is which, bailouts, the two trees and the pointer swap, where useLayoutEffect and useEffect actually run, and what happens to half-finished work when something urgent arrives. Every lesson also says why React is built that way and what the choice costs. The fibers are laid out the way the pointers point: child drops down, sibling goes right, return comes back up the left gutter, so reading the diagram is reading the data structure. A twelfth view is a captured profile rather than a drawing — the same 120-component update rendered plainly and inside startTransition, measured at 83.1 ms in one block against 5.5 ms in sixteen, for the same total work — and a Sources view lists every claim with the React file, the function and the constant it came from. Written for someone who has never opened React's source, and built so they do not have to take any of it on trust.

Built by Ananda Rizki, a frontend developer — one of the small web apps he writes for fun at Labs.