React Internals
Expert / StaffDiffing Algorithm Β· Fiber Reconciler Β· Rendering Lanes Β· useTransition Β· useDeferredValue
What this module covers
React's performance model rests on two pillars: the diffing algorithm (how React decides which DOM nodes to update) and rendering lanes (how React 18 prioritises concurrent work so high-priority updates β like a text input β are never blocked by slow, low-priority renders).The Diffing Algorithm
AdvancedHow React compares two virtual DOM trees in O(n) time
O(n) heuristic β 3 rules
A naive tree-diff is O(nΒ³). React gets to O(n) by making three assumptions:- Two elements of different types produce completely different trees β destroy & rebuild.
- Two elements of the same type at the same position β update props & recurse into children.
- Developer-supplied keys hint which list children are stable across renders.
// React's internal reconcileChildFibers pseudocode
function reconcile(current, workInProgress) {
if (current.type !== workInProgress.type) {
// Rule 1 β different type β unmount everything below current
unmountFiberTree(current)
return createFiberFromElement(workInProgress)
}
// Rule 2 β same type β clone fiber, patch props
const fiber = cloneFiber(current)
fiber.pendingProps = workInProgress.props
return fiber
}
// Rule 3 β keyed list reconciliation
function reconcileChildrenArray(current, newChildren) {
const existingByKey = mapRemainingChildren(current)
return newChildren.map(child =>
child.key
? existingByKey.get(child.key) ?? createFiberFromElement(child) // reuse or create
: createFiberFromElement(child) // no key β positional
)
}Rule 1 β Same component type β state preserved
When React sees the same component type at the same tree position across two renders it reuses the existing Fiber. Only props change β the component is not remounted, so useState, useRef, and subscriptions survive.
Step 1: click +1 a few times. Step 2: click Toggle Label.
β Count survives! React sees the same component type at the same position β updates props only, no remount.
Rule 1 (flip side) β Different type β full remount
Swap a SquareBox for a CircleBox at the same position and React tears down the entire subtree: cleanup effects run, state is lost, and a fresh instance is created.
Common pitfall β defining components inside render
If you define a component inside another component's function body, it gets a new reference on every parent render. React treats each reference as a different type β it unmounts and remounts the child on every single parent render. Always define components at the module top level.Step 1: click +1 a few times. Step 2: click Switch Shape.
β Count resets! Different component type β React unmounts the old tree and mounts a fresh one.
Rule 3 β Keys in lists
Without keys React reconciles lists positionally: indexΒ 0 of the old tree maps to indexΒ 0 of the new tree, regardless of which item is actually there. Adding a stable key lets React match items by identity, enabling moves instead of destroy-and-recreate.
When to use array index as key
advancedUsing array index as a key is only safe when the list is static and never reordered.
Use the interactive editor below β toggle keys on/off, increment counters, then shuffle to see the difference:
const COLORS = ['#1d4ed8','#15803d','#7c3aed','#b45309','#be185d'] function Item({ id, label, color }) { const [n, setN] = useState(0) return ( <div style={{ padding: '7px 12px', background: color, borderRadius: 6, color: '#fff', display: 'flex', alignItems: 'center', gap: 10, marginBottom: 5, fontSize: 13 }}> <span style={{ flex: 1 }}>{label}</span> <span style={{ background: 'rgba(0,0,0,0.3)', padding: '1px 8px', borderRadius: 4, fontSize: 12 }}>π {n}</span> <button onClick={() => setN(c => c + 1)} style={{ background: 'rgba(255,255,255,0.2)', border: 'none', color: 'white', padding: '2px 8px', borderRadius: 4, cursor: 'pointer' }}>+</button> </div> ) } function Demo() { const [items, setItems] = useState([ { id: 'apple', label: 'π Apple' }, { id: 'banana', label: 'π Banana' }, { id: 'cherry', label: 'π Cherry' }, { id: 'date', label: 'π΄ Date' }, ]) const [useKeys, setUseKeys] = useState(true) const shuffle = () => setItems(arr => [...arr].sort(() => Math.random() - 0.5)) const prepend = () => setItems(arr => [{ id: 'new-' + Date.now(), label: 'π New Item' }, ...arr]) return ( <div style={{ fontFamily: 'system-ui', padding: 14 }}> <div style={{ display: 'flex', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}> <button onClick={shuffle} style={{ background: '#4f46e5', color: '#fff', border: 'none', padding: '6px 12px', borderRadius: 5, cursor: 'pointer', fontSize: 12 }}> Shuffle </button> <button onClick={prepend} style={{ background: '#0369a1', color: '#fff', border: 'none', padding: '6px 12px', borderRadius: 5, cursor: 'pointer', fontSize: 12 }}> Prepend </button> <button onClick={() => setUseKeys(k => !k)} style={{ background: useKeys ? '#15803d' : '#b91c1c', color: '#fff', border: 'none', padding: '6px 12px', borderRadius: 5, cursor: 'pointer', fontSize: 12 }}> Keys: {useKeys ? 'ON β' : 'OFF β'} </button> </div> <p style={{ color: '#9ca3af', fontSize: 12, marginBottom: 8 }}> {useKeys ? 'β key=id β click + to set counts, then shuffle. Counts follow their item!' : 'β οΈ No keys β counts are tied to DOM position, not item identity. Shuffle to break things.'} </p> {useKeys ? items.map((item, i) => ( <Item key={item.id} id={item.id} label={item.label} color={COLORS[i % COLORS.length]} /> )) : items.map((item, i) => ( <Item label={item.label} color={COLORS[i % COLORS.length]} /> )) } </div> ) } render(<Demo />)
Reconciler output visualizer
Compare a before/after virtual DOM tree and see exactly what React marks as UPDATE, SKIP, MOVE, or CREATE:
const OLD_TREE = [ { type: 'div', key: null, props: 'className=container', status: null }, { type: 'h1', key: null, props: 'children=Hello', status: null }, { type: 'p', key: null, props: 'children=World', status: null }, { type: 'ul', key: null, props: '', status: null }, { type: 'li', key: 'a', props: 'Apple', status: null }, { type: 'li', key: 'b', props: 'Banana', status: null }, ] const NEW_TREE = [ { type: 'div', key: null, props: 'className=container updated', status: 'update' }, { type: 'h1', key: null, props: 'children=Hello React', status: 'update' }, { type: 'p', key: null, props: 'children=World', status: 'same' }, { type: 'ul', key: null, props: '', status: 'same' }, { type: 'li', key: 'b', props: 'Banana', status: 'move' }, { type: 'li', key: 'c', props: 'Cherry', status: 'create' }, ] const STATUS_CFG = { update: { bg: '#1d4ed8', label: 'UPDATE', icon: 'U' }, same: { bg: '#15803d', label: 'SKIP', icon: '-' }, create: { bg: '#166534', label: 'CREATE', icon: '+' }, move: { bg: '#92400e', label: 'MOVE', icon: 'M' }, } function NodeRow({ node, statusKey, index }) { const cfg = STATUS_CFG[statusKey] const tag = '<' + node.type + '>' const keyLabel = node.key ? 'key=' + node.key : '' return ( <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px', borderBottom: '1px solid #1f2937', fontSize: 12 }}> {cfg && ( <span style={{ background: cfg.bg, color: '#fff', borderRadius: 4, padding: '1px 6px', fontSize: 11, minWidth: 56, textAlign: 'center', flexShrink: 0 }}> {cfg.icon} {cfg.label} </span> )} <span style={{ color: '#67e8f9', fontFamily: 'monospace', flexShrink: 0 }}>{tag}</span> {keyLabel && ( <span style={{ color: '#f472b6', fontFamily: 'monospace', flexShrink: 0 }}>{keyLabel}</span> )} <span style={{ color: '#9ca3af', fontSize: 11, flex: 1 }}>{node.props}</span> </div> ) } function Demo() { const [show, setShow] = useState('diff') const tabs = ['diff', 'before', 'after'] const headings = { diff: "Reconciler output (O(n) comparison)", before: "Previous render β old Fiber tree", after: "New render β updated Fiber tree", } const tree = show === 'before' ? OLD_TREE : NEW_TREE return ( <div style={{ fontFamily: 'system-ui', padding: 14 }}> <div style={{ display: 'flex', gap: 6, marginBottom: 10 }}> {tabs.map(tab => ( <button key={tab} onClick={() => setShow(tab)} style={{ background: show === tab ? '#4f46e5' : '#1f2937', color: '#fff', border: 'none', padding: '5px 12px', borderRadius: 5, cursor: 'pointer', fontSize: 12 }}> {tab} </button> ))} </div> <div style={{ border: '1px solid #374151', borderRadius: 6, overflow: 'hidden' }}> <div style={{ background: '#1f2937', padding: '6px 10px', fontSize: 11, color: '#6b7280' }}> {headings[show]} </div> {show === 'diff' ? NEW_TREE.map((node, i) => ( <NodeRow key={i} node={node} statusKey={node.status} index={i} /> )) : tree.map((node, i) => ( <NodeRow key={i} node={node} statusKey={null} index={i} /> )) } </div> <p style={{ color: '#6b7280', fontSize: 11, marginTop: 8 }}> Diff tab shows what React does: UPDATE props, SKIP unchanged nodes, MOVE keyed items, CREATE new nodes. </p> </div> ) } render(<Demo />)
Rendering Lanes
ExpertReact 18 Concurrent Rendering β priority queues for UI work
Lane model (React 18+)
React 18 represents work priority as a 31-bit bitmask called lanes. Each bit represents a priority tier. During scheduling React picks the highest-priority lane and renders it first. Lower lanes can be deferred, batched, or even interrupted if something more urgent arrives.// Simplified lane constants (react-reconciler/src/ReactFiberLane.js)
const SyncLane = 0b0000000000000000000000000000001 // priority 1 (highest)
const InputContinuousLane = 0b0000000000000000000000000000100 // keyboard / pointer
const DefaultLane = 0b0000000000000000000000000010000 // normal setState
const TransitionLane1 = 0b0000000000000000000000001000000 // startTransition
const IdleLane = 0b0100000000000000000000000000000 // background work
// When you call startTransition(() => setState(...))
// React schedules the update on TransitionLane1 instead of DefaultLane
// β the Scheduler can postpone it if SyncLane work is pendinguseTransitionβ mark updates as non-urgent
startTransition wraps state updates that don't need to happen right now. React schedules them on a lower-priority lane. If a higher-priority update arrives (e.g. another keystroke) the transition is interrupted and restarted after the urgent work finishes.
useTransition signature
advancedReturns [isPending, startTransition] β isPending is true while the transition render is in-flight.
Type quickly β the input updates in an urgent lane, the list filters in a transition lane.
function buildList(query) { const list = [] for (let i = 0; i < 3000; i++) { const name = 'item-' + i if (!query || name.includes(query.toLowerCase())) list.push(name) } return list.slice(0, 60) } function Demo() { const [input, setInput] = useState('') const [filterQ, setFilterQ] = useState('') const [isPending, startTx] = useTransition() const handleChange = (e) => { const val = e.target.value setInput(val) // urgent lane β input updates instantly startTx(() => setFilterQ(val)) // transition lane β list can be interrupted } const results = buildList(filterQ) return ( <div style={{ fontFamily: 'system-ui', padding: 14 }}> <p style={{ color: '#9ca3af', fontSize: 12, margin: '0 0 8px' }}> Type quickly β the input field is in the <strong style={{ color: '#a78bfa' }}>urgent</strong> lane. The list render is in a <strong style={{ color: '#fbbf24' }}>transition</strong> lane and can be interrupted. </p> <input value={input} onChange={handleChange} placeholder="Filter 3 000 itemsβ¦" style={{ width: '100%', padding: '8px 12px', borderRadius: 6, border: '1px solid #374151', background: '#111827', color: '#f9fafb', fontSize: 13, marginBottom: 8, boxSizing: 'border-box' }} /> <div style={{ height: 22, color: '#fbbf24', fontSize: 12, marginBottom: 6 }}> {isPending && 'β³ Updating list in transition laneβ¦'} </div> <div style={{ display: 'flex', gap: 16, fontSize: 12, fontFamily: 'monospace', marginBottom: 8 }}> <span style={{ color: '#a78bfa' }}>input: <span style={{ color: '#fff' }}>"{input}"</span></span> <span style={{ color: '#fbbf24' }}>filterQ: <span style={{ color: '#fff' }}>"{filterQ}"</span></span> </div> <div style={{ maxHeight: 200, overflow: 'auto', border: '1px solid #374151', borderRadius: 6 }}> {results.map(item => ( <div key={item} style={{ padding: '3px 10px', color: '#d1d5db', fontSize: 12, borderBottom: '1px solid #1f2937' }}>{item}</div> ))} </div> </div> ) } render(<Demo />)
useDeferredValueβ deferred derived state
useDeferredValue accepts a value and returns a version of it that βlags behindβ by one render. React will re-render with the old value while scheduling a background re-render with the new one. Unlike useTransition, you don't need to own the state update β useful when you receive a prop or context value that drives an expensive child.
| Feature | useTransition | useDeferredValue |
|---|---|---|
| Own the state update? | β Yes β wraps setState | β No β wraps any value |
| API | startTransition(() => setState(β¦)) | const d = useDeferredValue(val) |
| isPending flag | β Built-in | β Must compare val !== d manually |
| Best for | Navigation, tab switches, search inputs | Expensive child renders receiving props |
useDeferredValue gives you a "lagging copy" of a value. React renders with the old value first, then re-renders with the new one when capacity allows.
function HeavyList({ query }) { const items = [] for (let i = 0; i < 2000; i++) { const name = 'entry-' + i if (!query || name.includes(query.toLowerCase())) items.push(name) } return ( <div style={{ maxHeight: 150, overflow: 'auto', border: '1px solid #374151', borderRadius: 6 }}> {items.slice(0, 40).map(it => ( <div key={it} style={{ padding: '3px 10px', color: '#d1d5db', fontSize: 12, borderBottom: '1px solid #1f2937' }}>{it}</div> ))} </div> ) } function Demo() { const [query, setQuery] = useState('') const deferred = useDeferredValue(query) const isStale = deferred !== query return ( <div style={{ fontFamily: 'system-ui', padding: 14 }}> <p style={{ color: '#9ca3af', fontSize: 12, margin: '0 0 8px' }}> useDeferredValue returns a lagging copy of the value. React shows your old results (dimmed) while re-rendering with new ones in the background. </p> <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Type to search 2 000 entriesβ¦" style={{ width: '100%', padding: '8px 12px', borderRadius: 6, border: '1px solid #374151', background: '#111827', color: '#f9fafb', fontSize: 13, marginBottom: 8, boxSizing: 'border-box' }} /> <div style={{ display: 'flex', gap: 16, fontSize: 12, fontFamily: 'monospace', marginBottom: 8 }}> <span style={{ color: '#67e8f9' }}> query: <span style={{ color: '#fff' }}>"{query}"</span> </span> <span style={{ color: isStale ? '#fbbf24' : '#4ade80' }}> deferred: <span style={{ color: '#fff' }}>"{deferred}"</span> {isStale && <span> β stale</span>} </span> </div> <div style={{ opacity: isStale ? 0.55 : 1, transition: 'opacity 0.2s' }}> <HeavyList query={deferred} /> </div> <p style={{ color: '#6b7280', fontSize: 11, marginTop: 6 }}> Try removing the deferred wrapper and passing query directly β notice the jank! </p> </div> ) } render(<Demo />)
Diffing β quick rules
- β’ Same type at same position β update, not remount
- β’ Different type β destroy subtree & rebuild
- β’ Always use stable IDs as keys, never array index in dynamic lists
- β’ Never define component functions inside render β they get a new reference every render
Lanes β quick rules
- β’ Wrap non-urgent setState in
startTransition - β’ Show
isPendingto give user feedback - β’ Use
useDeferredValuewhen you don't own the state - β’ Pair deferred value with
memoto avoid redundant renders - β’ Use
flushSynconly when you truly need synchronous DOM access