useDeferredValue and transitions: priority is a second render
useDeferredValue re-renders urgently with the old value, then runs an interruptible background render with the new one — two renders by design. Without memo downstream the deferral buys nothing. Unlike debounce there is no fixed delay: it adapts to device speed.
An internal admin tool had a search box over a 12,000-row client-side index, and typing in it dropped frames — each keystroke triggered an 80ms render of the filtered table, so INP on that field sat around 350ms, deep in the “poor” band. An engineer read about concurrent rendering, deleted the old 250ms debounce, and wrapped the query in useDeferredValue. The input felt exactly as janky as before. The Profiler told the story: every keystroke now produced two renders of the parent — and both of them re-rendered the 80ms table, because the table component was not memoized. useDeferredValue had done precisely what it promises: an urgent re-render with the old query first, then a background one with the new query. But the urgent render re-rendered the table anyway — same old query, no memo, no bailout — so the “fast” pass cost 80ms too. The deferral was working; nothing downstream was letting it pay off. One memo() wrapper later, the urgent render dropped to under 2ms, INP fell under 100ms, and the background render quietly restarted itself whenever the next keystroke interrupted it. The lesson the team wrote down: useDeferredValue is not a debounce — it is a second render, and it only buys you anything if the first one is cheap.
The honest mechanics: two renders, not one delayed render
When the value you pass to useDeferredValue changes, React does not wait and render once. It does this, in order:
- Urgent re-render with the OLD deferred value. The component re-renders immediately so the urgent part of the update — the controlled input echoing the keystroke — commits right away.
useDeferredValue(query)still returns the previous query in this pass. - Background re-render with the NEW value. React then schedules a second render where the deferred value catches up. This render runs at transition priority: it is interruptible. If another keystroke arrives mid-render, React abandons the in-progress background work and restarts it with the latest value — intermediate values are skipped, never committed.
So a single keystroke costs two renders of everything that reads the deferred value’s component. That is the deliberate price. The payoff exists only when the urgent render is nearly free, which is where the contract from the memoization lesson returns:
const deferredQuery = useDeferredValue(query);
// ❌ urgent render re-renders the expensive table anyway — deferral buys nothing
<HugeTable query={deferredQuery} />
// ✅ memo + unchanged prop in the urgent pass = bailout; only the bg render pays
const HugeTable = memo(function HugeTable({ query }) { /* 80ms of rows */ });In the urgent pass, deferredQuery is unchanged from the previous render — so a memoized HugeTable bails out via the shallow props comparison, and the urgent render costs microseconds. Without memo, the urgent pass re-renders 80ms of table with the old query — pure waste. This is the single most common way useDeferredValue “does not work” in production code.
A teammate wraps a slow chart's input data in useDeferredValue but the Profiler shows typing is still janky, with two slow renders per keystroke instead of one. What is the diagnosis?
Not a debounce: no fixed delay, adapts to the device
Debounce and useDeferredValue solve overlapping symptoms with different machinery, and the difference is measurable:
- Debounce picks a constant. 250ms means every user waits 250ms after their last keystroke — on an M3 MacBook where the render takes 12ms, you imposed 238ms of artificial latency; on a low-end Android where the render takes 400ms, the wait is 250ms plus a blocking 400ms render that freezes the input mid-burst.
- useDeferredValue has no timer. The background render starts as soon as React is free. On the fast machine it completes between keystrokes — results feel instant, no artificial delay. On the slow device, each keystroke interrupts the still-running background render, React restarts with the latest value, and the list simply lags while the input stays responsive — the lag automatically equals what the device can afford, no constant to tune.
The honest boundary: deferral only helps render cost. It cannot rate-limit network requests — the deferred value changes once per keystroke eventually, and if your fetch keys off it, you still fire per-keystroke requests at the server (just slightly later). Debounce/throttle remain the right tool when the expensive thing is a network call or server load, and the two compose: debounce the fetch, defer the render.
Lanes: what “urgent” actually means
Under the hood React schedules work in lanes — priority buckets. Discrete input events (keydown, click) render in a synchronous lane: they must commit before the browser paints again, because a controlled input that does not echo the character on the next frame reads as a broken keyboard. That is the definition of urgent — the user just expressed intent and the UI must acknowledge it now. Transitions (startTransition, useTransition, and the background half of useDeferredValue) render in transition lanes: interruptible, abandonable, restartable.
Interruption is restart-from-scratch, not pause-resume: when an urgent update lands mid-transition, React throws away the partially built tree and re-renders from the top with fresh state. That work is lost by design — which is why render functions must be pure and side-effect-free; a transition render may run and be discarded any number of times before one of them commits. It also means a long transition on a slow device can be starved by a fast typist: each keystroke kills the previous background render, and results only appear when typing pauses — which is exactly the UX you want, frames spent on the input rather than a list nobody can read mid-burst.
Numbers: why this is an INP story
INP — Interaction to Next Paint — grades the worst-case latency between an interaction and the next frame; under 200ms is “good”, above 500ms “poor”, and it is a Core Web Vitals field metric, measured on your users’ actual devices. A synchronous 350ms list render inside a keystroke handler is a 350ms+ INP on every keypress — there is no next paint until the render commits. Splitting that work with useDeferredValue moves the 350ms into an interruptible background render and leaves the urgent commit at single-digit milliseconds: INP drops to the cost of the cheap pass. This is also why “it is fast on my machine” fails as a test: the deferral’s value is largest exactly on the devices you do not develop on, where the background render gets interrupted dozens of times. Profile with 6x CPU throttling; watch INP in field data after shipping.
A search box fires an API request per query change, and the server team complains about request volume during typing. An engineer proposes replacing the existing 300ms debounce with useDeferredValue 'since it is the modern equivalent'. What is wrong?
useTransition vs useDeferredValue: which knob
Same machinery, different ownership of the state. useTransition is for when you own the setState — you wrap the update (startTransition(() => setQuery(q))) and get isPending for free. useDeferredValue is for when you receive a value you do not control — a prop, a context value, state owned elsewhere — and want a lagging copy to feed the expensive subtree. Comparing the two values (query !== deferredQuery) gives you the same pending signal. If you find yourself deferring a value whose setState you wrote three lines up, a transition is the cleaner spelling of the same intent.
- 01Describe the exact render sequence after a keystroke when the query feeds useDeferredValue, and state the downstream requirement without which the API is a net loss.
- 02Contrast useDeferredValue with debounce on a fast and a slow device, and explain why the choice between them is decided by what is actually expensive.
useDeferredValue is honestly described in one sentence that surprises most teams: it doubles your renders on purpose. The first render is urgent and runs with the old deferred value so the input can echo the keystroke before the next paint — that is what urgent means in React’s lane model: discrete input commits synchronously or the UI reads as broken. The second render runs at transition priority with the new value, and it is interruptible — another keystroke abandons it, work is discarded, and the render restarts with the latest value, which is why render purity matters and why intermediate states are never shown. The economics only work when the urgent pass is nearly free, and that requires memoization downstream: in the urgent render the deferred value is unchanged, so a memo wrapper lets the expensive subtree bail out; without it you pay the full render twice, the trap from the admin-tool incident where INP stayed at 350ms until one memo() dropped the urgent pass under 2ms. Against debounce the distinction is mechanical: debounce is a fixed timer that overcharges fast devices and undercharges slow ones, while deferral has no constant — it adapts, lagging exactly as much as the device requires. But deferral schedules renders, not requests: it cannot reduce server traffic, so debounce keeps that job and the two compose. Choose useTransition when you own the setState and want isPending; choose useDeferredValue when the value arrives from elsewhere. Verify in the Profiler with CPU throttling, and let field INP — good under 200ms — be the scoreboard. Now when you see a search box that feels janky despite a useDeferredValue wrapper, open the Profiler first: if you see two slow renders per keystroke instead of one fast and one slow, the fix is a single memo() on the expensive subtree, not a different API.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.