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
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.
Further Reading
- How to Deploy a React App to Vercel in 10 Minutes
- 10 JavaScript Tricks That Made Me a Better Developer Overnight
- The JavaScript Roadmap for 2025: What to Learn and in What Order
- Tailwind CSS vs Bootstrap in 2025: The Final Verdict
- How to Pass a JavaScript Interview at Google, Meta, or Amazon
- HTML5 and CSS3 for Beginners: Building Your First Real Website
- Django vs Flask in 2025: Which Framework Should You Learn?
- How Python Is Used in Finance: Real Wall Street Use Cases
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.