Must knowReact Performance · 03

Concurrent Rendering

useDeferredValue + memo keeps typing instant over a slow grid; without memo it does nothing (recorded).

On this page · 7 sections
  1. In one minute
  2. The example: a search over a slow grid
  3. How it works
  4. Common mistakes
  5. Interview Q&A
  6. Related
  7. Sources

In one minute

At 60 frames per second, the browser gets a new frame every ~16 ms. A render that takes 125 ms freezes the page for 125 ms: the letter you just typed doesn’t even appear. Concurrent rendering lets React split a low-priority render into small pieces, pause to let the browser handle input and paint, and throw the render away if newer input arrives. You tell React what’s low-priority with useTransition (around a state update) or useDeferredValue (around a value). The work doesn’t get smaller, it stops blocking. The catch with useDeferredValue: the slow component must be memoized, or React renders it anyway during the urgent render.

You’ll be able to: keep an input responsive over a slow list with useDeferredValue and memo, and explain why it does nothing without memo.

Plain
Search.tsx
1function ProductGrid({ query }) {2  const results = catalog.filter(matches(query)).slice(0, 120)3  return results.map(p => <SlowCard key={p.id} product={p} />)4}5function SlowCard({ product }) {6  burn(1) // ~1 ms each: a stand-in for an expensive card7  return <li>{product.name}</li>8}910function Search() {11  const [query, setQuery] = useState('')12  return (<>13    <input value={query} onChange={e => setQuery(e.target.value)} />14    <ProductGrid query={query} />15  </>)16}
What you see
Next frame after the key: –
Rendered this step
ProductGrid–
  1. event<input value={query} onChange={e => setQuery(e.target.value)} />Type “c”.
  2. render phase<ProductGrid query={query} />One synchronous render with the input and all 120 slow cards. Nothing can interrupt it.
  3. browserOnly now can the browser show the typed letter. Recorded: next frame after about 125–135 ms, on every key.

0 / 0
A search box over a grid of 120 cards that take about 1 ms each. The recordings type “c”, “e”, “r” and measure how soon the browser could paint after each key (all three recorded).

The example: a search over a slow grid

The grid shows up to 120 cards, and each card takes about 1 ms to render (a stand-in for a genuinely expensive component). The recordings type “c”, “e”, “r” with 150 ms between keys, and log how long after each keystroke the browser could paint its next frame.

function ProductGrid({ query }: { query: string }) {
	const results = catalog.filter((p) => p.name.toLowerCase().includes(query)).slice(0, 120)
	log(`  ProductGrid rendered for "${query}"`)
	return (
		<ul className="grid">
			{results.map((p) => (
				<SlowCard key={p.id} product={p} />
			))}
		</ul>
	)
}

function SlowCard({ product }: { product: Product }) {
	burn(1) // pretend this card is expensive to render
	return <li>{product.name}</li>
}

1. Plain

function SearchPlain() {
	const [query, setQuery] = useState('')
	return (
		<>
			<input id="search" value={query} onChange={(e) => setQuery(e.target.value)} />
			<ProductGrid query={query} />
		</>
	)
}
keystroke "c": next frame after 132 ms
keystroke "e": next frame after 129 ms
keystroke "r": next frame after 127 ms

Every keystroke waits for the whole grid: the input lags by over 100 ms per letter.

2. useDeferredValue without memo

function SearchDeferredNoMemo() {
	const [query, setQuery] = useState('')
	const deferredQuery = useDeferredValue(query)
	return (
		<>
			<input id="search" value={query} onChange={(e) => setQuery(e.target.value)} />
			<ProductGrid query={deferredQuery} />
		</>
	)
}
keystroke "c": next frame after 129 ms
keystroke "e": next frame after 124 ms
keystroke "r": next frame after 124 ms

No better. The renders explain why:

  ProductGrid rendered for ""
  ProductGrid rendered for "c"
  ProductGrid rendered for "c"
  ProductGrid rendered for "ce"
  ProductGrid rendered for "ce"
  ProductGrid rendered for "cer"

The urgent render passed the grid the old value, but ProductGrid isn’t memoized, so it rendered anyway, all 120 slow cards, for a query it had already shown. Then the background render did it again with the new value.

3. useDeferredValue with memo

function SearchDeferred() {
	const [query, setQuery] = useState('')
	const deferredQuery = useDeferredValue(query)
	const stale = query !== deferredQuery
	return (
		<>
			<input id="search" value={query} onChange={(e) => setQuery(e.target.value)} />
			<div style={{ opacity: stale ? 0.6 : 1 }}>
				<MemoGrid query={deferredQuery} />
			</div>
		</>
	)
}
keystroke "c": next frame after 7 ms
keystroke "e": next frame after 1 ms
keystroke "r": next frame after 14 ms
  ProductGrid rendered for "c"
  ProductGrid rendered for "ce"
  ProductGrid rendered for "cer"

Now the urgent render gives MemoGrid the same query as before, memo skips it, and the input paints within about one frame. The grid renders once per value, in the background, and is dimmed while it’s stale.

How it works

Common mistakes

Interview Q&A

What is concurrent rendering?

React’s ability to render in interruptible pieces, yielding to the browser between them, and to abandon a render when newer input arrives. The DOM changes only when a whole render is done.

Urgent vs non-urgent updates?

Urgent updates reflect direct input (typing, clicking) and must feel instant. Non-urgent ones change what’s shown (filtered results, a new tab) and can lag a bit. You mark non-urgent ones with useTransition or useDeferredValue.

Why must the slow component be memoized for useDeferredValue to help?

The urgent render still renders the component that calls useDeferredValue, and its children with it. Only memo, seeing the same old prop, skips the slow child. Recorded: without memo, each keystroke still took over 100 ms and the grid rendered twice per keystroke; with it, about one frame or less.

useTransition or useDeferredValue?

useTransition when you call the state setter; useDeferredValue when you only receive the value.

Deferring vs debouncing?

Debouncing delays the work by a fixed time and still blocks when it runs. Deferring starts right after the urgent render, runs in interruptible chunks, and adapts to the device’s speed.

Sources

Primary source: react.dev/reference/react/useDeferredValue