The React Performance Guide: Making Your App Blazing Fast
React performance optimization guide: memo, useMemo, useCallback, lazy loading, virtualization, bundle splitting, and profiling — with real examples.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
The React Performance Guide: Making Your App Blazing Fast
I optimized a product page that was taking 3.2 seconds to become interactive. The culprit: a single component was re-rendering 47 times on initial load because of a chain of poorly-placed state and missing memoization.
After profiling, the fixes took 20 minutes. Time to interactive dropped to 0.9 seconds.
Performance optimization in React is not about magic — it's about understanding why React re-renders, measuring where time is actually spent, and applying targeted fixes. Premature optimization is the root of a lot of wasted effort in React codebases.
This guide gives you the mental model, the measurement tools, and the optimization techniques. In that order.
First: Understand the React Render Cycle
React re-renders a component when:
- Its state changes
- Its parent re-renders (default behavior)
- Its context value changes
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<Child /> {/* Re-renders EVERY time Parent re-renders, even though Child has no props */}
</div>
);
}
Re-rendering is not inherently bad — React is designed to handle many re-renders efficiently. The problem is when:
- A component is expensive to render (complex calculations, large DOM trees)
- A component re-renders when its props haven't changed
Step 1: Measure Before You Optimize
React DevTools Profiler shows you what actually re-renders and how long it takes:
- Install React DevTools browser extension
- Open DevTools → Profiler tab
- Click "Record", interact with your app, click "Stop"
- Inspect the flame graph — slow renders appear in red/orange
Don't skip this step. I've seen developers add useMemo everywhere to "optimize" when profiling shows the actual bottleneck is a network request or a third-party library.
Technique 1: React.memo — Skip Re-Renders
React.memo wraps a component so it only re-renders when its props actually change:
// Without memo: re-renders whenever parent re-renders
function TaskItem({ task, onToggle }: { task: Task; onToggle: (id: number) => void }) {
console.log('TaskItem rendered:', task.id);
return (
<div onClick={() => onToggle(task.id)}>
{task.text} — {task.done ? '✓' : '○'}
</div>
);
}
// With memo: only re-renders when task or onToggle changes
const TaskItem = React.memo(function TaskItem({ task, onToggle }) {
console.log('TaskItem rendered:', task.id);
return (
<div onClick={() => onToggle(task.id)}>
{task.text} — {task.done ? '✓' : '○'}
</div>
);
});
The catch: React.memo does a shallow comparison of props. If you pass a new object or function reference on every render, memo doesn't help.
function TaskList() {
const [tasks, setTasks] = useState([...]);
// NEW function created on every render — memo is useless
const handleToggle = (id: number) => {
setTasks(tasks.map(t => t.id === id ? { ...t, done: !t.done } : t));
};
return tasks.map(task => (
<TaskItem key={task.id} task={task} onToggle={handleToggle} />
// TaskItem re-renders anyway because handleToggle is new each render
));
}
Fix with useCallback:
Technique 2: useCallback — Stable Function References
function TaskList() {
const [tasks, setTasks] = useState([...]);
// Stable reference — same function object across renders
const handleToggle = useCallback((id: number) => {
setTasks(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
}, []); // Empty array: function never changes
return tasks.map(task => (
<TaskItem key={task.id} task={task} onToggle={handleToggle} />
// Now React.memo works correctly
));
}
Note: handleToggle uses prev (functional state update) instead of tasks directly — this avoids capturing stale state.
Technique 3: useMemo — Cache Expensive Calculations
function ProductList({ products, searchQuery, sortBy }: Props) {
// This filters and sorts on EVERY render
const displayProducts = products
.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
return a.name.localeCompare(b.name);
});
// BETTER: only recalculate when products, searchQuery, or sortBy changes
const displayProducts = useMemo(() =>
products
.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
return a.name.localeCompare(b.name);
}),
[products, searchQuery, sortBy]
);
return displayProducts.map(product => <ProductCard key={product.id} product={product} />);
}
Only use useMemo when the calculation is genuinely expensive (sorting/filtering thousands of items, complex computations). For simple array operations on small arrays, the useMemo overhead outweighs the benefit.
Technique 4: Code Splitting with React.lazy
import React, { Suspense, lazy } from 'react';
// Load AdminDashboard only when it's first rendered
const AdminDashboard = lazy(() => import('./AdminDashboard'));
const HeavyChart = lazy(() => import('./HeavyChart'));
const SettingsPanel = lazy(() => import('./SettingsPanel'));
function App() {
const [view, setView] = useState<'home' | 'admin' | 'settings'>('home');
return (
<div>
<nav>{/* navigation */}</nav>
<Suspense fallback={<div className="loading">Loading...</div>}>
{view === 'admin' && <AdminDashboard />}
{view === 'settings' && <SettingsPanel />}
{view === 'home' && <HeavyChart />}
</Suspense>
</div>
);
}
The admin dashboard, chart library, and settings panel are only downloaded when the user navigates to them. Initial bundle size — and Time to Interactive — improves significantly.
Technique 5: Virtualize Long Lists
Rendering 1,000 DOM nodes is slow. Render only what's visible:
npm install react-window
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }: { items: Item[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style} className="list-item">
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600} // Visible height of the list
itemCount={items.length}
itemSize={50} // Height of each item in px
width="100%"
>
{Row}
</FixedSizeList>
);
}
For variable-height items, use VariableSizeList. For tables, consider react-virtuoso which handles both.
Technique 6: State Colocation
Move state down to where it's used. Unnecessary high-level state causes unnecessary re-renders:
// Bad: count state in parent causes entire parent to re-render
function ExpensiveParent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<VeryExpensiveComponent /> {/* Re-renders on every count change */}
</div>
);
}
// Good: extract counter into its own component
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
function ExpensiveParent() {
return (
<div>
<Counter /> {/* Counter state stays inside Counter */}
<VeryExpensiveComponent /> {/* Never re-renders due to count */}
</div>
);
}
This pattern — colocating state — is often more impactful than adding memo or useMemo.
Performance Checklist
- Profile first with React DevTools before optimizing
- Use production build for accurate measurements (
npm run build && npx serve dist) - Colocate state — don't lift state higher than necessary
- Use
React.memofor list items in large lists - Pair
React.memowithuseCallbackfor stable function props - Use
useMemoonly for genuinely expensive calculations - Lazy load large components not needed on initial render
- Virtualize lists with more than 100–200 items
- Analyze bundle size with
npm run build -- --mode analyze
For the React fundamentals that these optimizations build on, see our React hooks tutorial. State management choices significantly impact performance — our React state management 2025 guide covers how Zustand and Jotai minimize unnecessary re-renders. And for the full React app architecture that influences performance, our React vs Next.js vs Remix guide covers rendering strategy decisions.
Frequently Asked Questions
When should I use React.memo?
List items in large lists, expensive components with stable props. Don't use it everywhere — measure first.
What causes unnecessary re-renders?
Parent re-renders, new object/array/function references on each render, context changes.
What is React.lazy?
Code splitting — components load as separate chunks only when first rendered. Reduces initial bundle size and improves Time to Interactive.
What is list virtualization?
Rendering only visible list items. Required for lists with 100–200+ items to maintain scroll performance.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
How to Deploy a React App to Vercel in 10 Minutes
Deploy a React app to Vercel in 10 minutes: from npm create vite to live URL, custom domain setup, environment variables, and preview deployments.
GraphQL vs REST: Which API Style Should You Learn in 2025?
GraphQL vs REST API compared honestly for 2025: when each makes sense, real code examples, and which API style to learn first as a developer.
JavaScript Promises and Async/Await: Finally Understand Them
JavaScript async await and Promises explained clearly: the event loop, Promise chains, async/await patterns, error handling, and common mistakes to avoid.
How to Pass a JavaScript Interview at Google, Meta, or Amazon
How to pass a JavaScript interview at top tech companies: closures, event loop, promises, DOM questions, system design, and real interview questions answered.