Keeping Large-List Search Responsive with useDeferredValue
A controlled search input that synchronously filters thousands of rows can block the browser from painting each keystroke. Here is how useDeferredValue separates urgent input updates from non-urgent list rendering, what it does not fix, and how to verify the improvement.
28 Mar 2026, 02:07 UTC

When a search input filters a list of 10,000 rows, every keystroke can feel stuck. The cursor lags, characters appear late, and the page freezes for a few hundred milliseconds. The cause is usually not the filter logic itself, but where React schedules the work: the input update and the expensive list render are both treated as urgent, so React may finish rendering the entire list before the browser paints the keystroke.
The fix is to tell React that the input is urgent and the list is not. In React 18 and later, useDeferredValue returns a deferred copy of a value that can lag behind during rapid updates. That lets the browser paint the input immediately while the list catches up in a lower-priority render.
Why typing feels stuck
React updates are not all equal. When you type into a controlled input, the onChange handler calls setState. If that same render pass also filters and mounts thousands of DOM nodes, React can block the main thread until the whole tree is committed. The browser cannot paint the new character until that work finishes. Under CPU throttling—which simulates a slower device—this input-to-paint latency becomes obvious.
For explicit control over urgency, useTransition returns startTransition and isPending; updates inside startTransition are interruptible. When you only need to defer a derived value, useDeferredValue is the simpler choice: it gives you a lagging copy of a value without wrapping every update in a callback.
A concrete pattern
Keep the input controlled by the immediate state, and derive a deferred copy only for the expensive filtering. Do not wrap the input's own setState in startTransition; that can make typing feel worse by delaying the input itself.
import { useState, useDeferredValue, useMemo } from 'react';
function SearchableList({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query, '');
const rows = useMemo(
() => items.filter(item =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase())
),
[items, deferredQuery]
);
const isStale = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search items..."
/>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{rows.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
</div>
);
}
Here, query drives the input so keystrokes stay immediate. deferredQuery drives the filter. While query !== deferredQuery, the list is intentionally showing stale results; dropping the opacity gives the user a visual cue that an update is in flight. Check your exact React version: the optional second argument to useDeferredValue and React Compiler availability vary by release.
Add useMemo only after profiling shows the filter is expensive. Blanket memoization adds noise, and if your project adopts React Compiler, automatic memoization may already cover the baseline.
What deferring does not fix
Deferring changes scheduling, not speed. If filtering 20,000 items takes 200 ms of JavaScript, useDeferredValue will not make that computation faster. It only ensures the keystroke paints first. If the list render is still expensive after deferring, optimize the data structure, add virtualization for very large lists, or move heavy computation off the main thread.
Transitions are also not a data-fetching tool. They do not cancel network requests or prevent race conditions. If your search hits an API, you still need request cancellation or logic to ignore stale responses. A deferred value only affects rendering; it says nothing about which response is correct.
Verifying the improvement
Before adding the hook, reproduce the problem. Build a list of 5,000–20,000 rows, open browser DevTools, and throttle the CPU to 4x or 6x slowdown. Measure input-to-paint latency with and without useDeferredValue. Use the React DevTools Profiler to confirm the list render is scheduled as a non-urgent update and that the input commit happens first. Add a temporary onRender callback to verify that deferredQuery eventually matches query after typing stops.
Repeat the measurements in a production build. Development mode and StrictMode double-render, which can exaggerate costs and make scheduling look worse than it is.
The practical takeaway
Add useDeferredValue only after the Profiler identifies a slow list commit. Keep the input immediate, derive the deferred copy for filtering, and verify under throttling that keystrokes paint first. If the list is still too large for the main thread, scheduling is not the final answer—virtualization or a web worker may be.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.