š 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.
useDeferredValue
The 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>;});
memo
React 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!
useDeferredValue
useDeferredValue
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
Quick Links
Legal Stuff
Social Media