React performance problems are predictable, and so are their solutions. 90% of React slowdowns come from four causes: unnecessary component re-renders, missing memoization on expensive computations, rendering 10,000 items in a list instead of virtualizing, and shipping 5MB JavaScript bundles. This guide covers every optimization technique with clear guidance on when to apply each.
⚡ TL;DR: Profile with React DevTools Profiler before optimizing. Use
React.memoto prevent child re-renders. UseuseMemofor expensive computations. UseuseCallbackfor stable function references. Virtualize lists over 100 items. Code-split withReact.lazy. Never define objects/arrays inline in JSX.
Profile first with React DevTools
// Enable React DevTools Profiler:
// 1. Open DevTools → Profiler tab
// 2. Click Record
// 3. Interact with slow UI
// 4. Click Stop
// 5. Look for: components with long render times, components
// that render when they shouldn't, renders triggered by parent
// The Flamegraph shows:
// - Gray bars: did not render this commit
// - Yellow/orange bars: slow renders
// - How many ms each component took
// - Why it re-rendered (props changed? state changed? parent re-rendered?)
React.memo — prevent unnecessary re-renders
// Without memo: re-renders every time parent renders
function ExpensiveChild({ data }) {
// Heavy computation or large render tree
return {data.map(item => )};
}
// With memo: only re-renders when data prop changes
const ExpensiveChild = React.memo(function({ data }) {
return {data.map(item => )};
});
// Custom comparison for deep equality:
const ExpensiveChild = React.memo(function({ user }) {
return ;
}, (prevProps, nextProps) => {
// Return true = do NOT re-render
return prevProps.user.id === nextProps.user.id &&
prevProps.user.updatedAt === nextProps.user.updatedAt;
});
// When to use React.memo:
// - Component renders often
// - Component has expensive render
// - Props rarely change
// When NOT to use:
// - Component always re-renders with new props anyway
// - Comparison cost > render cost (simple components)
useMemo and useCallback correctly
// useMemo: memoize expensive computed values
function ProductList({ products, searchQuery, sortBy }) {
// Without useMemo: recalculates on every render
// With useMemo: only recalculates when products, searchQuery, or sortBy changes
const filteredAndSorted = useMemo(() => {
return products
.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1);
}, [products, searchQuery, sortBy]);
return {filteredAndSorted.map(p => )}
;
}
// useCallback: stable function reference for child components
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback: new function instance every render
// Child with memo would still re-render because handler is "new"
const handleClick = useCallback((id) => {
deleteItem(id);
}, []); // Empty deps: function never changes
return ;
}
// MISTAKE: useMemo for everything — adds overhead
// Only use useMemo when:
// - Computation takes >1ms (filter/sort large arrays, complex transforms)
// - Value passed as prop to memoized child component
The inline object/array problem
// EVERY render creates a NEW object reference
// React.memo children will ALWAYS re-render
// Bad: new object on every render
setHovered(true)} // New function reference every render
/>
// Good: stable references
const CHART_STYLE = { color: "red", margin: 20 }; // Module level (never changes)
const DEFAULT_DATA = [1, 2, 3]; // Module level
function Parent() {
const handleHover = useCallback(() => setHovered(true), []);
return ;
}
List virtualization — only render visible items
import { FixedSizeList } from 'react-window';
// BAD: renders 10,000 DOM nodes
function BadList({ items }) {
return (
{items.map(item => )}
);
// 10,000 DOM nodes: 3-5 second initial render, 500MB memory
}
// GOOD: renders only ~20 visible items (the viewport)
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
);
return (
{Row}
);
// Only ~8 DOM nodes visible at once, regardless of list size
// Rule: virtualize any list with >100 items
}
Code splitting with React.lazy
import { lazy, Suspense } from 'react';
// Without code splitting: entire dashboard loaded on first visit
import HeavyDashboard from './Dashboard';
// With code splitting: Dashboard only loaded when navigated to
const HeavyDashboard = lazy(() => import('./Dashboard'));
const ReportsPage = lazy(() => import('./Reports'));
const AdminPanel = lazy(() => import('./Admin'));
function App() {
return (
}>
} />
} />
);
}
// Initial bundle: 200KB instead of 2MB
// Dashboard chunk: loaded on demand (~1.5MB separate)
React performance checklist
- ✅ Profile with React DevTools Profiler before adding any optimization
- ✅
React.memofor components that render often with stable props - ✅
useMemofor expensive computations (filter/sort of large arrays) - ✅
useCallbackfor functions passed as props to memoized children - ✅ Never define objects, arrays, or functions inline in JSX props
- ✅ Virtualize any list with more than 100 items (react-window or react-virtual)
- ✅ Code-split every route with
React.lazy+Suspense - ✅ Use
keycorrectly — stable unique IDs, never array index - ❌ Don’t use
useMemofor cheap operations — the overhead can exceed the saving - ❌ Don’t memoize everything — profile first, optimize second
React performance is closely tied to JavaScript fundamentals — the WeakMap and WeakRef guide explains the memory model that makes React memo’s shallow comparison fast. For build-time optimization, V8 JIT optimization explains why monomorphic component props compile faster. External reference: React rendering docs.
Recommended Books
→ Designing Data-Intensive Applications — The essential deep-dive on distributed systems, databases, and production engineering at scale.
→ The Pragmatic Programmer — Timeless principles for writing better code, debugging smarter, and advancing as an engineer.
Affiliate links. We earn a small commission at no extra cost to you.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
