βš›οΈ

React Internals

Expert / Staff

Diffing 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

Advanced

How 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:
  1. Two elements of different types produce completely different trees β†’ destroy & rebuild.
  2. Two elements of the same type at the same position β†’ update props & recurse into children.
  3. 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.

Same component type β†’ state is preservedLive Demo

Step 1: click +1 a few times. Step 2: click Toggle Label.

Label A: 0

βœ… 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.
Different component type β†’ count resets (remount)Live Demo

Step 1: click +1 a few times. Step 2: click Switch Shape.

β–  Square: 0

✘ 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

advanced

Using 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:

keys-in-lists.jsx β€” toggle keys, shuffle, see the diff
LIVE
editor
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 />)
preview

Reconciler output visualizer

Compare a before/after virtual DOM tree and see exactly what React marks as UPDATE, SKIP, MOVE, or CREATE:

reconciler-visualiser.jsx β€” before Β· after Β· diff
LIVE
editor
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 />)
preview
πŸ›£οΈ

Rendering Lanes

Expert

React 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 pending

useTransitionβ€” 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

advanced

Returns [isPending, startTransition] β€” isPending is true while the transition render is in-flight.

useTransition β€” live preview

Type quickly β€” the input updates in an urgent lane, the list filters in a transition lane.

input (urgent): ""query (transition): ""
item-0
item-1
item-2
item-3
item-4
item-5
item-6
item-7
item-8
item-9
item-10
item-11
item-12
item-13
item-14
item-15
item-16
item-17
item-18
item-19
item-20
item-21
item-22
item-23
item-24
item-25
item-26
item-27
item-28
item-29
item-30
item-31
item-32
item-33
item-34
item-35
item-36
item-37
item-38
item-39
item-40
item-41
item-42
item-43
item-44
item-45
item-46
item-47
item-48
item-49
item-50
item-51
item-52
item-53
item-54
item-55
item-56
item-57
item-58
item-59
useTransition.jsx β€” edit live
LIVE
editor
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 />)
preview

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.

FeatureuseTransitionuseDeferredValue
Own the state update?βœ… Yes β€” wraps setState✘ No β€” wraps any value
APIstartTransition(() => setState(…))const d = useDeferredValue(val)
isPending flagβœ… Built-in✘ Must compare val !== d manually
Best forNavigation, tab switches, search inputsExpensive child renders receiving props
useDeferredValue β€” live preview

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.

query: ""deferred: ""
item-0
item-1
item-2
item-3
item-4
item-5
item-6
item-7
item-8
item-9
item-10
item-11
item-12
item-13
item-14
item-15
item-16
item-17
item-18
item-19
item-20
item-21
item-22
item-23
item-24
item-25
item-26
item-27
item-28
item-29
item-30
item-31
item-32
item-33
item-34
item-35
item-36
item-37
item-38
item-39
item-40
item-41
item-42
item-43
item-44
item-45
item-46
item-47
item-48
item-49
item-50
item-51
item-52
item-53
item-54
item-55
item-56
item-57
item-58
item-59
useDeferredValue.jsx β€” edit live
LIVE
editor
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 />)
preview

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 isPending to give user feedback
  • β€’ Use useDeferredValue when you don't own the state
  • β€’ Pair deferred value with memo to avoid redundant renders
  • β€’ Use flushSync only when you truly need synchronous DOM access