React 19 Performance Secrets, Concurrent Rendering, Suspense & Real Tips
I've been shipping React apps for over a decade. I've led teams through slow UIs, fought with render waterfalls, and learned most of these lessons the har...
15 Nov 2025

I've been shipping React apps for over a decade. I've led teams through slow UIs, fought with render waterfalls, and learned most of these lessons the hard way. React 19 and its concurrent features changed how I think about performance. Instead of micro-optimizing individual renders, I now design for responsiveness and progressive loading.
These are the practical lessons I use on projects today -- things I keep teaching at PersiaJS meetups and on NoghteVorood.
Why responsiveness beats raw speed
Users don't measure render time. They measure how snappy the UI feels. A UI that stays interactive during a heavy computation feels faster than one that freezes for 200ms, even if the total computation time is the same.
Concurrent rendering makes this possible. React can interrupt low-priority work to handle urgent updates (like a keystroke), then resume the interrupted work later.
But React doesn't do this automatically for all state updates. You have to tell it which updates are low priority.
useTransition in practice
Wrap non-urgent updates in startTransition. The most common use case: filtering or searching a large dataset.
const [query, setQuery] = useState('')
const [results, setResults] = useState<Item[]>(allItems)
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
setQuery(value) // urgent: update the input field now
startTransition(() => {
setResults(allItems.filter(item => item.name.includes(value))) // can wait
})
}
The input stays responsive. The filtered list catches up when React has time. If the user types another character before filtering finishes, React throws away the stale work. No wasted renders.
Suspense as architecture
Suspense is more than a loading spinner. It's a way to structure your app so that components declare what they need and React orchestrates when to show them.
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<Feed />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
Each section loads independently. The header can render before the feed is ready. The sidebar can render before the header is ready. No waterfall. No global loading state that blocks everything.
The mistakes I see
Over-transitioning. Not every state update needs useTransition. If updating a counter takes 1ms, wrapping it in a transition adds overhead without benefit. Use transitions for updates that trigger expensive renders.
Giant Suspense boundaries. A single <Suspense> around your entire app means the user sees one big spinner. Split boundaries so independent sections can load independently.
Ignoring the profiler. React DevTools profiler shows you exactly which components re-render and how long they take. Use it before applying any optimization. Most of the time, the bottleneck is somewhere you don't expect.
The trade-offs
Concurrent rendering introduces subtle bugs. Components can render, get interrupted, and render again with different props. If you have side effects during render (external API calls, DOM mutations), they might execute twice or not at all.
Suspense requires your data layer to support it. Not every library does. React Query and Next.js have good support. Custom fetch hooks often don't.
These are powerful tools. But they add complexity. Apply them where they solve a real measured problem, not preemptively.