Using useTransition and useDeferredValue to Improve INP
This guide is part of React Rendering Performance, within Framework Performance. Some updates must feel instant: the character you typed appearing in the input, the checkbox toggling, the tab highlighting. Others can take a moment: the filtered list of 2,000 results, the newly selected tab's heavy content, a chart redrawing. In React's default synchronous rendering, both kinds of work happen together, and the slow part blocks the fast part — the input lags because the list is rendering.
React's concurrent features separate them. useTransition marks state updates as non-urgent: React renders them in the background, yielding to the browser so it can paint and handle input, and abandons a stale render if newer input arrives. useDeferredValue gives a lagging copy of a value: components using the deferred copy re-render at lower priority, while those using the current value update immediately. For INP, this means the interaction's next paint shows the urgent update quickly, even when the full update takes longer.
Rapid Diagnosis
- Find interactions with high INP where the handler sets state that drives heavy rendering.
- Profile the interaction: a long commit dominated by a large subtree (lists, charts, tab content).
- Check whether the urgent feedback is part of the same update (input value, selected state).
- Check React version: concurrent features require React 18 or later with
createRoot.
Root Cause Analysis
1. One update for urgent and non-urgent UI. Input value and results update together.
2. Large subtrees on each interaction. Lists and charts re-render synchronously.
3. No yielding. Synchronous renders block the main thread until complete.
4. Stale work. Fast typing triggers renders for intermediate values that are immediately outdated.
Step-by-Step Resolution
1. Wrap non-urgent updates in startTransition
function Filters({ onChange }) {
const [active, setActive] = useState('all');
const [isPending, startTransition] = useTransition();
function select(value) {
setActive(value); // urgent: button state
startTransition(() => onChange(value)); // non-urgent: heavy list update
}
return <FilterButtons active={active} onSelect={select} pending={isPending} />;
}
// trade-off: the list shows old results briefly; show isPending (dimmed list or
// spinner) so users know the update is in progress.
2. Defer values passed to expensive children
function SearchPage({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.6 : 1 }}><Results items={items} query={deferredQuery} /></div>
</>);
}
const Results = React.memo(function Results({ items, query }) { /* heavy filtering + rendering */ });
// trade-off: Results must be memoized, or it re-renders with the urgent update too.
3. Use transitions for tab and route changes
Wrap tab switches and navigation state changes in transitions so the old content stays interactive while the new content renders.
4. Make the heavy render cheaper too
Transitions defer work; combine them with memoization, virtualization and moving expensive computation out of render.
Verification
Record the interaction in the Performance panel with CPU throttling: the first paint after input should occur quickly, followed by background rendering in smaller chunks rather than one long task. The Interactions track should show a short duration for the interaction. In the field, INP for the target should drop.
Worked Example: A Product Search Page
A retailer's search page filtered 3,000 products client-side as users typed. Each keystroke rendered the input and the full result grid synchronously: 220ms per keystroke on a mid-range phone, with visibly lagging input and INP p75 of 310ms. The team passed a deferred query to a memoized results component, dimmed results while stale, and virtualized the grid. Keystrokes painted in under 30ms; results updated in the background, skipping intermediate queries when users typed quickly. INP p75 fell to 110ms, and search abandonment dropped.
How Concurrent Rendering Yields
During a transition, React renders in small units of work and checks whether it should yield to the browser after each. If an urgent update arrives (another keystroke), React pauses or discards the in-progress transition render and handles the urgent update first. Because the main thread is released between units, the browser can paint and respond to input. This is why transitions improve INP: the next paint after an interaction is not blocked by the entire non-urgent render. It also means non-urgent renders may never complete if urgent updates keep arriving — which is fine for intermediate search queries.
Choosing Which Updates Are Urgent
The decision is a product question as much as a technical one: what must the user see immediately to know their action registered? Usually that is the direct feedback on the control they touched — the character in the input, the pressed state of the button, the checked checkbox, the highlighted tab. Everything derived from it — results, totals, charts, the content of the selected tab — can follow a moment later, as long as there is a pending indicator. Write this down for each key interaction, and structure state so that the urgent part lives in its own state variable, updated outside the transition.
Common Mistakes
- Wrapping the input value itself in a transition. The input lags; keep it urgent.
- Deferred value without memoization. The child re-renders with the urgent value anyway.
- No pending indicator. Users think the click did nothing.
- Expecting transitions to speed up blocking code. Synchronous loops inside render still block.
Edge Cases
Controlled inputs. Input state must update synchronously; transitions are for derived updates.
Suspense and transitions. Transitions keep showing old content instead of fallbacks when new content suspends, avoiding jarring loading states.
Server Actions. In frameworks with server actions, useTransition tracks pending state for the action.
Older React versions. Concurrent features need React 18+ and the concurrent root API.
FAQ
What is the difference between useTransition and useDeferredValue?
useTransition marks a state update as non-urgent where you set it. useDeferredValue creates a lagging copy of a value for components that should update at lower priority.
Do transitions improve INP?
Yes, when the slow part of an interaction is a large re-render. The urgent update paints quickly, and the expensive render no longer blocks the next paint.
Can I use transitions for data fetching?
Yes. With Suspense-enabled data sources or server actions, transitions keep the current UI visible while new data loads.
Why does my input still lag?
The input's own state may be inside the transition, or the expensive child is not memoized and re-renders with the urgent update.
Is useDeferredValue the same as debouncing?
No. Debouncing waits a fixed time before updating; deferred values render as soon as possible at lower priority and skip stale work automatically.
Do transitions work on the server?
They are a client-side scheduling feature. On the server, rendering happens once per request.
How do I show that an update is pending?
Use isPending from useTransition, or compare the current and deferred values, and dim or annotate the content that is still updating.
Can transitions make total rendering slower?
Slightly, due to yielding overhead, but users perceive the app as faster because urgent feedback is immediate.
Do transitions help on fast devices?
Less noticeably, because renders finish quickly anyway. Their value shows on mid-range and low-end phones, where INP problems concentrate.
Can I use transitions with external state stores?
Stores read via useSyncExternalStore update synchronously, so transitions cannot defer them. Keep the derived, expensive state in React state if you want transitions to apply.
Related
- Stopping context re-render cascades — reduce what renders.
- Optimizing INP with scheduler.yield — yielding outside React.
- Virtualizing long lists in React — cheaper renders for long lists.