What JSX compiles to: a plain object naming a type, a key and some props. It is not a component instance and it is not a DOM node — nothing has happened yet. Creating one is nearly free, which is what makes re-rendering a whole tree affordable.
{ $$typeof: Symbol(react.element),
type: Row, key: '3', props: {…} }The mutable counterpart to an element: one node per component, holding its type, its host node, its hook state, and links to return, child and sibling instead of a children array — a linked list React can walk one node at a time.
fiber.return / .child / .sibling
fiber.memoizedState // the hook list
The render phase
the interruptible half
beginWork walks down calling your components and reconciling their children, completeWork walks back up. It changes nothing outside React, so the loop can stop between any two fibers, let something more urgent through, and start over. That is why render must stay pure.
while (workInProgress && !shouldYield())
performUnitOfWork()
Double buffering
the two trees
Each fiber keeps an alternate — its counterpart from the last commit. The next tree is built by cloning into those alternates, so the tree currently on screen stays whole and renderable the entire time. An abandoned render costs nothing to throw away.
wip = current.alternate ?? clone(current)
Matching children is deliberately shallow: same position and same type, reuse the fiber; different type, drop the subtree and rebuild it. A key overrides position, so a reordered list moves fibers rather than rebuilding them, and unchanged props let a subtree be skipped entirely.
items.map(i => <Row key={i.id} />)Commit & lanes
the control
Commit is one synchronous pass over the flagged fibers — mutations applied, refs attached, layout effects run before paint, useEffect flushed after it. Which render gets to run at all is decided by lanes: a click is urgent, a startTransition update is not and can be abandoned mid-flight.
startTransition(() => setQuery(q))
// mutation → layout → paint → passive