Back to Blog
ReactPerformanceOptimizationFrontend

React Performance Optimization Techniques

Make React apps fast. From memo to lazy loading to profiling to avoiding common pitfalls.

B
Bootspring Team
Engineering
December 20, 2022
6 min read

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 together

useMemo 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 children

Code 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.

Share this article

Help spread the word about Bootspring