React is fast by default, but complex apps can slow down. Here's how to identify and fix performance issues.
Measuring Performance#
1// React DevTools Profiler
2// 1. Open React DevTools
3// 2. Go to Profiler tab
4// 3. Click Record
5// 4. Interact with app
6// 5. Stop and analyze
7
8// Programmatic profiling
9import { Profiler, ProfilerOnRenderCallback } from 'react';
10
11const onRender: ProfilerOnRenderCallback = (
12 id,
13 phase,
14 actualDuration,
15 baseDuration,
16 startTime,
17 commitTime
18) => {
19 console.log({
20 id,
21 phase,
22 actualDuration,
23 baseDuration,
24 });
25};
26
27function App() {
28 return (
29 <Profiler id="App" onRender={onRender}>
30 <MyComponent />
31 </Profiler>
32 );
33}
34
35// Why Did You Render library
36import whyDidYouRender from '@welldone-software/why-did-you-render';
37
38whyDidYouRender(React, {
39 trackAllPureComponents: true,
40});React.memo#
1// Prevent re-renders when props don't change
2const ExpensiveList = React.memo(function ExpensiveList({
3 items,
4 onSelect,
5}: {
6 items: Item[];
7 onSelect: (id: string) => void;
8}) {
9 return (
10 <ul>
11 {items.map((item) => (
12 <li key={item.id} onClick={() => onSelect(item.id)}>
13 {item.name}
14 </li>
15 ))}
16 </ul>
17 );
18});
19
20// Custom comparison function
21const UserCard = React.memo(
22 function UserCard({ user }: { user: User }) {
23 return <div>{user.name}</div>;
24 },
25 (prevProps, nextProps) => {
26 // Return true if props are equal (skip re-render)
27 return prevProps.user.id === nextProps.user.id;
28 }
29);
30
31// When NOT to use memo
32// - Component usually receives different props
33// - Component is cheap to render
34// - Props are primitives that change togetheruseMemo and useCallback#
1// useMemo - Memoize expensive calculations
2function ProductList({ products, filter }: Props) {
3 // Only recalculate when products or filter change
4 const filteredProducts = useMemo(() => {
5 return products.filter((p) => p.category === filter);
6 }, [products, filter]);
7
8 const sortedProducts = useMemo(() => {
9 return [...filteredProducts].sort((a, b) => a.price - b.price);
10 }, [filteredProducts]);
11
12 return <List items={sortedProducts} />;
13}
14
15// useCallback - Memoize functions
16function Parent() {
17 const [count, setCount] = useState(0);
18
19 // Without useCallback, new function every render
20 // Child would re-render even if count didn't change
21 const handleClick = useCallback((id: string) => {
22 console.log('clicked', id);
23 }, []); // Empty deps = stable reference
24
25 return <Child onClick={handleClick} />;
26}
27
28// When to use useCallback
29// - Passing callbacks to memoized children
30// - Callbacks in dependency arrays
31// - Expensive child re-renders
32
33// When NOT to use
34// - Simple components
35// - Callbacks that need fresh values
36// - No memoized childrenCode Splitting#
1// Lazy loading components
2import { lazy, Suspense } from 'react';
3
4// Instead of: import Dashboard from './Dashboard';
5const Dashboard = lazy(() => import('./Dashboard'));
6const Settings = lazy(() => import('./Settings'));
7
8function App() {
9 return (
10 <Suspense fallback={<Loading />}>
11 <Routes>
12 <Route path="/dashboard" element={<Dashboard />} />
13 <Route path="/settings" element={<Settings />} />
14 </Routes>
15 </Suspense>
16 );
17}
18
19// Named exports
20const Dashboard = lazy(() =>
21 import('./Dashboard').then((module) => ({
22 default: module.Dashboard,
23 }))
24);
25
26// Preloading
27const DashboardPromise = import('./Dashboard');
28const Dashboard = lazy(() => DashboardPromise);
29
30// Preload on hover
31function NavLink({ to, children }: Props) {
32 const preload = () => {
33 if (to === '/dashboard') {
34 import('./Dashboard');
35 }
36 };
37
38 return (
39 <Link to={to} onMouseEnter={preload}>
40 {children}
41 </Link>
42 );
43}Virtualization#
1// For long lists, only render visible items
2import { FixedSizeList } from 'react-window';
3
4function VirtualList({ items }: { items: Item[] }) {
5 const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
6 <div style={style}>
7 {items[index].name}
8 </div>
9 );
10
11 return (
12 <FixedSizeList
13 height={400}
14 width={300}
15 itemCount={items.length}
16 itemSize={35}
17 >
18 {Row}
19 </FixedSizeList>
20 );
21}
22
23// Variable size list
24import { VariableSizeList } from 'react-window';
25
26function VariableList({ items }: { items: Item[] }) {
27 const getItemSize = (index: number) => {
28 return items[index].height || 50;
29 };
30
31 return (
32 <VariableSizeList
33 height={400}
34 width={300}
35 itemCount={items.length}
36 itemSize={getItemSize}
37 >
38 {Row}
39 </VariableSizeList>
40 );
41}
42
43// Grid virtualization
44import { FixedSizeGrid } from 'react-window';
45
46function VirtualGrid({ items, columnCount }: Props) {
47 const Cell = ({ columnIndex, rowIndex, style }: CellProps) => {
48 const index = rowIndex * columnCount + columnIndex;
49 const item = items[index];
50
51 return item ? <div style={style}>{item.name}</div> : null;
52 };
53
54 return (
55 <FixedSizeGrid
56 columnCount={columnCount}
57 rowCount={Math.ceil(items.length / columnCount)}
58 columnWidth={100}
59 rowHeight={100}
60 width={400}
61 height={400}
62 >
63 {Cell}
64 </FixedSizeGrid>
65 );
66}State Management#
1// Avoid unnecessary state updates
2function Form() {
3 // ❌ Bad - causes re-render on every keystroke
4 const [form, setForm] = useState({ name: '', email: '', bio: '' });
5
6 // ✓ Better - use refs for values that don't need re-render
7 const nameRef = useRef<HTMLInputElement>(null);
8
9 // ✓ Or split state
10 const [name, setName] = useState('');
11 const [email, setEmail] = useState('');
12}
13
14// State colocation
15// ❌ Bad - state too high in tree
16function App() {
17 const [selectedId, setSelectedId] = useState<string | null>(null);
18
19 return (
20 <div>
21 <Header />
22 <Sidebar selectedId={selectedId} onSelect={setSelectedId} />
23 <Content selectedId={selectedId} />
24 </div>
25 );
26}
27
28// ✓ Better - move state closer to where it's used
29function Sidebar() {
30 const [selectedId, setSelectedId] = useState<string | null>(null);
31
32 return (
33 <div>
34 <SidebarNav selectedId={selectedId} onSelect={setSelectedId} />
35 <SidebarContent selectedId={selectedId} />
36 </div>
37 );
38}Avoiding Common Pitfalls#
1// ❌ Creating objects/arrays in render
2function Bad({ items }) {
3 return <List style={{ margin: 10 }} items={[...items]} />;
4}
5
6// ✓ Move outside or memoize
7const listStyle = { margin: 10 };
8
9function Good({ items }) {
10 const sortedItems = useMemo(() => [...items].sort(), [items]);
11 return <List style={listStyle} items={sortedItems} />;
12}
13
14// ❌ Inline function in JSX
15function Bad() {
16 return <Button onClick={() => doSomething()} />;
17}
18
19// ✓ Use useCallback or define outside
20function Good() {
21 const handleClick = useCallback(() => doSomething(), []);
22 return <Button onClick={handleClick} />;
23}
24
25// ❌ Index as key with dynamic lists
26function Bad({ items }) {
27 return items.map((item, index) => <Item key={index} {...item} />);
28}
29
30// ✓ Use stable unique ID
31function Good({ items }) {
32 return items.map((item) => <Item key={item.id} {...item} />);
33}Image Optimization#
1// Lazy load images
2function LazyImage({ src, alt }: Props) {
3 return (
4 <img
5 src={src}
6 alt={alt}
7 loading="lazy"
8 decoding="async"
9 />
10 );
11}
12
13// Next.js Image component
14import Image from 'next/image';
15
16function OptimizedImage() {
17 return (
18 <Image
19 src="/hero.jpg"
20 alt="Hero"
21 width={800}
22 height={400}
23 priority // Above the fold
24 placeholder="blur"
25 blurDataURL="data:image/jpeg;base64,..."
26 />
27 );
28}Best Practices#
Measuring:
✓ Profile before optimizing
✓ Use React DevTools Profiler
✓ Set performance budgets
✓ Test on slow devices
Rendering:
✓ Use React.memo appropriately
✓ Virtualize long lists
✓ Code split routes
✓ Lazy load below-the-fold
State:
✓ Keep state close to usage
✓ Avoid unnecessary updates
✓ Use refs for non-render values
✓ Split large state objects
Conclusion#
React performance optimization starts with measurement. Use the Profiler to identify slow renders, apply React.memo judiciously, virtualize long lists, and code split routes. Most importantly, don't optimize prematurely—profile first, then fix actual bottlenecks.