🚀 Smooth React UIs with Concurrent Rendering and useDeferredValue
When building modern web apps, we often hit a critical UX bottleneck: performance during complex renders. Whether you’re filtering long lists or loading content dynamically, sluggish UIs can frustrate users and break immersion.
React 18 introduced concurrent rendering to help address this. In this post, we’ll explore what concurrent rendering is, how it works, and how you can use tools like useDeferredValue and memo to keep your app responsive—even under load.
To feel smooth, a web app should update at 60 frames per second. That gives you around 16 milliseconds per frame to complete your JavaScript work—including rendering your React components.
If React spends more than that on rendering, the browser can’t paint the next frame in time, leading to jank, input lag, and stuttering animations.
But what if your app legitimately has too much to do in one frame? Enter concurrent rendering.
Concurrent rendering is a new architecture in React 18 that allows React to pause a render that’s taking too long, let the browser catch up, and then resume rendering later—without blocking user interactions.
Imagine React slicing up rendering work into chunks. While rendering, React checks: “Am I taking too long?” If yes, it yields to the browser and picks up where it left off when there’s time.
This way, expensive renders like large lists don’t freeze the UI.
To learn more about the theory behind this, check out:
React is smart—but it doesn’t know which parts of your UI are high priority. For example, a user typing in an input should feel snappy, while rendering a giant list of filtered results can wait.
You need to tell React what to deprioritize—and that’s where useDeferredValue comes in.
useDeferredValueThe useDeferredValue hook lets you flag certain values as less urgent. It tells React: “Render this later if something more important is happening.”
Here’s an example from the official React docs:
function App() {const [text, setText] = useState('');const deferredText = useDeferredValue(text); // ← deprioritize thisreturn (<><input value={text} onChange={e => setText(e.target.value)} /><SlowList text={deferredText} /></>);}const SlowList = memo(function SlowList({ text }) {// Simulate slow renderinglet start = performance.now();while (performance.now() - start < 50) {} // ⏱️ artificial delayreturn <div>{text}</div>;});
memoReact can only skip re-rendering slow components with stale data if they’re memoized.
If you don’t wrap the component in React.memo, React will have to re-render it anyway—even with stale data—which defeats the purpose of deferring the value.
Let’s say you’re building a card search interface:
<Card /> components filters and re-renders on each keystroke.This is the setup for jank—especially if you filter hundreds of items in real-time.
First, wrap your slow Card component with memo:
const Card = memo(function Card({ data }) {// Simulate heavy renderlet start = performance.now();while (performance.now() - start < 30) {}return <div>{data.title}</div>;});
This tells React: “Only re-render if props actually change.”
If you’re filtering the card list using a search query, you can defer that query like this:
function CardGrid({ query, data }) {const deferredQuery = useDeferredValue(query);const filteredData = useMemo(() => data.filter(card => card.title.includes(deferredQuery)),[deferredQuery, data]);return (<div className="grid">{filteredData.map(item => (<Card key={item.id} data={item} />))}</div>);}
Now, the user can type freely into the search input, and React will pause filtering and card rendering if the browser needs to remain responsive.
💡 Typing feels instant, even with expensive components!
useDeferredValueuseDeferredValue shines when:
However:
Suspense and startTransition.Use the React DevTools Profiler to verify:
| Technique | Use Case |
|---|---|
useDeferredValue() | Deprioritize rendering of large or slow updates |
React.memo() | Prevent re-renders when props haven’t changed |
useMemo() | Avoid recalculating expensive logic unnecessarily |
startTransition() | Mark updates as low-priority (especially for data fetching) |
React’s concurrent rendering model gives you more control over prioritizing user experience. With tools like useDeferredValue and memo, you can balance performance and responsiveness—even when your app needs to do heavy lifting.
If your app feels slow when rendering filtered results or lists, it’s time to let React pause, breathe, and catch up. Defer the less important work, and give the user control center stage.
📚 Further Reading
useDeferredValue