Md Arshad Khan | Full Stack Engineer Logo
React,  frontend,  Next js

The “Modern” React Performance Checklist for 2026

Author

Md Arshad Khan

Date Published

blog-hero-image

Look, we need to talk about React performance. Not the scary kind where someone drops a 47-point checklist on you, but the real-world kind where your boss asks why the trading dashboard freezes when you scroll through 10,000 rows, or why users are rage-clicking buttons that don’t respond fast enough.

This is your practical performance checklist for 2026. No hype, no “just rewrite everything,” just what actually works.

First things first: Profile before you panic

Before you start sprinkling useMemo everywhere like seasoning on bland code, open the React DevTools Profiler and actually see what is slow. You might think your massive table is the problem, but it could just be that one component that rerenders 47 times because someone passed an inline arrow function as a prop.

The Profiler shows you how long each component takes to render and how often it rerenders. Yellow bars are your enemies. Blue bars are your friends. If you see a component lighting up like a Christmas tree on every keystroke, you found your culprit.

Memoization: The hooks everyone misuses

Here is the truth about React 19 and memoization: the new React Compiler does make things better automatically, but it is not magic. You still need to understand when and why to memoize.

When to actually use React.memo

Wrap a component with React.memo when it receives the same props often but still rerenders because its parent changed. Classic case: a list of items where each item component is identical but the parent list state changes.

When to actually use React.memo

Wrap a component with React.memo when it receives the same props often but still rerenders because its parent changed. Classic case: a list of items where each item component is identical but the parent list state changes.

1const ExpensiveRow = React.memo(({ item, onDelete }) => {
2 console.log('Rendering row for', item.id);
3 return (
4 <div className="row">
5 <span>{item.name}</span>
6 <button onClick={() => onDelete(item.id)}>Delete</button>
7 </div>
8 );
9});
10

Without React.memo, all 500 rows rerender when you delete one. With it, only the deleted row updates. That is the difference between smooth and janky.

useMemo: For expensive calculations only

Do not wrap every variable in useMemo. That actually makes things slower because memoization has overhead. Use it when you have genuinely expensive calculations like sorting 10,000 financial records or computing aggregate stats.

1// Good use case
2const sortedTransactions = useMemo(() => {
3 return transactions
4 .filter(t => t.status === 'completed')
5 .sort((a, b) => b.amount - a.amount)
6 .slice(0, 100);
7}, [transactions]);
8
9// Bad use case (just wasting memory)
10const fullName = useMemo(() => firstName + ' ' + lastName, [firstName, lastName]);
11

The second example runs faster without useMemo. String concatenation is cheap. Array operations on thousands of items are not.

useCallback: Stop the cascade

Use useCallback when passing functions to memoized child components or as dependencies in other hooks. Every time your component renders, inline functions get recreated with new references, which breaks memoization.

1const ParentComponent = () => {
2 const [filter, setFilter] = useState('');
3
4 // Without useCallback, this recreates on every render
5 // and breaks React.memo on ExpensiveList
6 const handleItemClick = useCallback((id) => {
7 console.log('Clicked', id);
8 }, []);
9
10 return <ExpensiveList items={items} onClick={handleItemClick} />;
11};
12

React 19 makes this less critical in some cases, but if you see your memoized components still rerendering, check your callbacks first.

Virtualization: When you have more rows than patience

If you are rendering more than 100 rows, virtualization is not optional. It is mandatory. The concept is simple: only render what is visible on screen plus a small buffer. Everything else is fake height to maintain scroll position.

TanStack Virtual is the current go-to library for this.

1import { useVirtualizer } from '@tanstack/react-virtual';
2
3function VirtualizedList({ items }) {
4 const parentRef = useRef(null);
5
6 const virtualizer = useVirtualizer({
7 count: items.length,
8 getScrollElement: () => parentRef.current,
9 estimateSize: () => 35, // estimated row height
10 });
11
12 return (
13 <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
14 <div style={{ height: virtualizer.getTotalSize() }}>
15 {virtualizer.getVirtualItems().map(virtualRow => (
16 <div
17 key={virtualRow.index}
18 style={{
19 position: 'absolute',
20 top: 0,
21 left: 0,
22 transform: `translateY(${virtualRow.start}px)`,
23 }}
24 >
25 {items[virtualRow.index].name}
26 </div>
27 ))}
28 </div>
29 </div>
30 );
31}
32

With this, 10,000 rows renders like 20 rows because that is all your browser actually draws. Scroll performance stays buttery smooth because React is not reconciling thousands of DOM nodes on every frame.

React Server Components: The performance trick with a learning curve

Server Components in Next.js App Router can genuinely improve initial load times, but only if you do it right. The research shows that just migrating to App Router without changing your data fetching can actually make performance worse.

What actually works

Server Components reduce JavaScript bundle size by keeping heavy components on the server. They also let you fetch data closer to your database without a round trip to the client.

1// app/dashboard/page.tsx (Server Component)
2async function DashboardPage() {
3 // This runs on the server, no loading spinners needed
4 const data = await fetchDashboardData();
5
6 return (
7 <div>
8 <ServerSideHeader data={data.header} />
9 <ClientInteractiveChart data={data.metrics} />
10 </div>
11 );
12}
13
14// components/ClientInteractiveChart.tsx
15'use client';
16export function ClientInteractiveChart({ data }) {
17 const [selected, setSelected] = useState(null);
18 // Interactive client-side logic
19}
20

The key insight: fetch data in Server Components, handle interactivity in Client Components, and use Suspense boundaries to stream content progressively.

When not to bother

If your app is mostly interactive, Server Components will not help much. A heavily interactive trading dashboard that updates in real-time should stay client-side. Server Components shine for content-heavy pages with minimal interactivity, like dashboards with mostly static data that refreshes on navigation.

Concurrent features: Keep the UI responsive

React 18 introduced useTransition for marking updates as low-priority so urgent updates stay smooth.

1function SearchResults() {
2 const [query, setQuery] = useState('');
3 const [results, setResults] = useState([]);
4 const [isPending, startTransition] = useTransition();
5
6 const handleSearch = (value) => {
7 setQuery(value); // Urgent: update input immediately
8
9 startTransition(() => {
10 // Non-urgent: filter can happen in background
11 const filtered = expensiveFilterOperation(data, value);
12 setResults(filtered);
13 });
14 };
15
16 return (
17 <>
18 <input value={query} onChange={e => handleSearch(e.target.value)} />
19 {isPending ? <Spinner /> : <ResultsList results={results} />}
20 </>
21 );
22}
23

The input stays responsive while the expensive filter operation runs in the background. Users see their typing immediately, not after the filter completes. This is the difference between feeling instant and feeling laggy.

The actual checklist

When your app feels slow, walk through this:

Profile first with React DevTools to find the actual bottleneck, not what you think it is

Check if components rerender unnecessarily and wrap with React.memo if they get the same props repeatedly

Add useMemo only for expensive calculations like large array operations or complex computations

Use useCallback for functions passed to memoized children or used as hook dependencies

Virtualize any list over 100 items using TanStack Virtual

Consider Server Components for data-heavy pages with low interactivity

Use useTransition to keep urgent updates responsive during heavy background work

Performance optimization is not about using every technique. It is about using the right technique for your specific bottleneck. Profile, fix what is actually slow, and move on to shipping features. Your users care about smooth interactions, not your memoization strategy.