Back to Blog
ReactPerformanceOptimizationMemoization

React.memo and Performance Optimization

Optimize React performance with memo, useMemo, and useCallback. From basics to advanced patterns to profiling strategies.

B
Bootspring Team
Engineering
October 7, 2021
7 min read

React re-renders can slow down applications. Here's how to optimize with memoization.

Understanding Re-renders#

1// Parent re-render causes child re-render 2function Parent() { 3 const [count, setCount] = useState(0); 4 5 return ( 6 <div> 7 <button onClick={() => setCount(c => c + 1)}> 8 Count: {count} 9 </button> 10 {/* ExpensiveChild re-renders even though props haven't changed */} 11 <ExpensiveChild name="John" /> 12 </div> 13 ); 14} 15 16function ExpensiveChild({ name }: { name: string }) { 17 console.log('ExpensiveChild rendered'); 18 // Expensive computation or complex UI 19 return <div>Hello, {name}</div>; 20}

React.memo Basics#

1// Wrap component to prevent unnecessary re-renders 2const ExpensiveChild = React.memo(function ExpensiveChild({ 3 name 4}: { 5 name: string 6}) { 7 console.log('ExpensiveChild rendered'); 8 return <div>Hello, {name}</div>; 9}); 10 11// Now only re-renders when props change 12 13// With custom comparison 14const MemoizedComponent = React.memo( 15 function MyComponent({ user, settings }) { 16 return ( 17 <div> 18 <span>{user.name}</span> 19 <span>{settings.theme}</span> 20 </div> 21 ); 22 }, 23 (prevProps, nextProps) => { 24 // Return true if props are equal (skip re-render) 25 return ( 26 prevProps.user.id === nextProps.user.id && 27 prevProps.settings.theme === nextProps.settings.theme 28 ); 29 } 30);

useCallback for Functions#

1function Parent() { 2 const [count, setCount] = useState(0); 3 4 // Without useCallback: new function every render 5 const handleClick = () => { 6 console.log('clicked'); 7 }; 8 9 // With useCallback: same function reference 10 const memoizedHandleClick = useCallback(() => { 11 console.log('clicked'); 12 }, []); // Empty deps = never changes 13 14 // With dependencies 15 const handleIncrement = useCallback(() => { 16 setCount(c => c + 1); 17 }, []); // setCount is stable 18 19 const handleSubmit = useCallback((data: FormData) => { 20 submitForm(data, count); 21 }, [count]); // Re-create when count changes 22 23 return ( 24 <div> 25 <button onClick={() => setCount(c => c + 1)}> 26 Count: {count} 27 </button> 28 {/* MemoizedChild won't re-render */} 29 <MemoizedChild onClick={memoizedHandleClick} /> 30 </div> 31 ); 32} 33 34const MemoizedChild = React.memo(function Child({ 35 onClick 36}: { 37 onClick: () => void 38}) { 39 console.log('Child rendered'); 40 return <button onClick={onClick}>Click me</button>; 41});

useMemo for Values#

1function ExpensiveComponent({ items, filter }: Props) { 2 // Without useMemo: recalculates every render 3 const filteredItems = items.filter(item => 4 item.name.includes(filter) 5 ); 6 7 // With useMemo: only recalculates when deps change 8 const memoizedItems = useMemo(() => { 9 console.log('Filtering items...'); 10 return items.filter(item => item.name.includes(filter)); 11 }, [items, filter]); 12 13 // Expensive computation 14 const statistics = useMemo(() => { 15 console.log('Computing statistics...'); 16 return { 17 total: items.length, 18 average: items.reduce((a, b) => a + b.value, 0) / items.length, 19 max: Math.max(...items.map(i => i.value)), 20 }; 21 }, [items]); 22 23 // Memoized object to prevent child re-renders 24 const config = useMemo(() => ({ 25 theme: 'dark', 26 size: 'large', 27 }), []); // Never changes 28 29 return ( 30 <div> 31 <Stats data={statistics} /> 32 <List items={memoizedItems} config={config} /> 33 </div> 34 ); 35}

Common Patterns#

1// Pattern 1: Memoized Context Value 2function ThemeProvider({ children }: { children: React.ReactNode }) { 3 const [theme, setTheme] = useState('light'); 4 5 // Memoize context value 6 const value = useMemo(() => ({ 7 theme, 8 setTheme, 9 isDark: theme === 'dark', 10 }), [theme]); 11 12 return ( 13 <ThemeContext.Provider value={value}> 14 {children} 15 </ThemeContext.Provider> 16 ); 17} 18 19// Pattern 2: Callback with Refs 20function SearchInput({ onSearch }: { onSearch: (query: string) => void }) { 21 const [query, setQuery] = useState(''); 22 const onSearchRef = useRef(onSearch); 23 24 // Update ref without re-creating callback 25 useEffect(() => { 26 onSearchRef.current = onSearch; 27 }, [onSearch]); 28 29 const handleSearch = useCallback(() => { 30 onSearchRef.current(query); 31 }, [query]); // Only depends on query 32 33 return ( 34 <input 35 value={query} 36 onChange={e => setQuery(e.target.value)} 37 onKeyDown={e => e.key === 'Enter' && handleSearch()} 38 /> 39 ); 40} 41 42// Pattern 3: Derived State 43function UserList({ users }: { users: User[] }) { 44 const [sortKey, setSortKey] = useState<keyof User>('name'); 45 const [filterActive, setFilterActive] = useState(false); 46 47 // Compose memoized operations 48 const activeUsers = useMemo( 49 () => filterActive ? users.filter(u => u.isActive) : users, 50 [users, filterActive] 51 ); 52 53 const sortedUsers = useMemo( 54 () => [...activeUsers].sort((a, b) => 55 String(a[sortKey]).localeCompare(String(b[sortKey])) 56 ), 57 [activeUsers, sortKey] 58 ); 59 60 return ( 61 <div> 62 <select onChange={e => setSortKey(e.target.value as keyof User)}> 63 <option value="name">Name</option> 64 <option value="email">Email</option> 65 </select> 66 <UserTable users={sortedUsers} /> 67 </div> 68 ); 69}

Component Composition#

1// Pattern: Move state down 2// Bad: Entire component re-renders 3function App() { 4 const [search, setSearch] = useState(''); 5 6 return ( 7 <div> 8 <input value={search} onChange={e => setSearch(e.target.value)} /> 9 <ExpensiveTree /> 10 </div> 11 ); 12} 13 14// Good: Only SearchInput re-renders 15function App() { 16 return ( 17 <div> 18 <SearchInput /> 19 <ExpensiveTree /> 20 </div> 21 ); 22} 23 24function SearchInput() { 25 const [search, setSearch] = useState(''); 26 return <input value={search} onChange={e => setSearch(e.target.value)} />; 27} 28 29// Pattern: Lift content up 30// Bad: ColorPicker changes cause ExpensiveTree re-render 31function App() { 32 return ( 33 <ColorProvider> 34 <ExpensiveTree /> 35 </ColorProvider> 36 ); 37} 38 39function ColorProvider({ children }) { 40 const [color, setColor] = useState('blue'); 41 42 return ( 43 <div style={{ color }}> 44 <input value={color} onChange={e => setColor(e.target.value)} /> 45 {children} {/* children don't re-render */} 46 </div> 47 ); 48}

Virtualization#

1import { FixedSizeList } from 'react-window'; 2 3// Virtualize long lists 4function VirtualizedList({ items }: { items: Item[] }) { 5 const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => ( 6 <div style={style}> 7 <ItemRow item={items[index]} /> 8 </div> 9 ), [items]); 10 11 return ( 12 <FixedSizeList 13 height={600} 14 width="100%" 15 itemCount={items.length} 16 itemSize={50} 17 > 18 {Row} 19 </FixedSizeList> 20 ); 21} 22 23// With memoized rows 24const ItemRow = React.memo(function ItemRow({ item }: { item: Item }) { 25 return ( 26 <div className="item-row"> 27 <span>{item.name}</span> 28 <span>{item.value}</span> 29 </div> 30 ); 31});

Profiling Performance#

1// Use React DevTools Profiler 2 3// Add Profiler component 4import { Profiler } from 'react'; 5 6function App() { 7 const onRenderCallback = ( 8 id: string, 9 phase: 'mount' | 'update', 10 actualDuration: number, 11 baseDuration: number, 12 startTime: number, 13 commitTime: number 14 ) => { 15 console.log({ 16 id, 17 phase, 18 actualDuration, 19 baseDuration, 20 }); 21 }; 22 23 return ( 24 <Profiler id="App" onRender={onRenderCallback}> 25 <ExpensiveComponent /> 26 </Profiler> 27 ); 28} 29 30// Why did you render (development tool) 31// npm install @welldone-software/why-did-you-render 32import React from 'react'; 33 34if (process.env.NODE_ENV === 'development') { 35 const whyDidYouRender = require('@welldone-software/why-did-you-render'); 36 whyDidYouRender(React, { 37 trackAllPureComponents: true, 38 }); 39} 40 41// Mark specific components 42MyComponent.whyDidYouRender = true;

When NOT to Memoize#

1// Don't memoize cheap operations 2// Bad: useMemo overhead > computation cost 3const doubled = useMemo(() => count * 2, [count]); 4// Good: Just compute it 5const doubled = count * 2; 6 7// Don't memoize when props always change 8// Bad: New object every render anyway 9function Parent() { 10 const [items, setItems] = useState([]); 11 12 // user is new object every render 13 const user = { name: 'John', items }; 14 15 // Memoization is useless here 16 return <MemoizedChild user={user} />; 17} 18 19// Good: Memoize the object too 20function Parent() { 21 const [items, setItems] = useState([]); 22 23 const user = useMemo(() => ({ 24 name: 'John', 25 items 26 }), [items]); 27 28 return <MemoizedChild user={user} />; 29} 30 31// Don't wrap everything in memo 32// Bad: Premature optimization 33const Button = React.memo(({ onClick, children }) => ( 34 <button onClick={onClick}>{children}</button> 35)); 36 37// Good: Only memoize when there's a measured problem

Advanced Patterns#

1// Selector pattern for context 2const UserContext = createContext<User[]>([]); 3 4function useUser(id: string) { 5 const users = useContext(UserContext); 6 7 // Only re-render when this specific user changes 8 return useMemo( 9 () => users.find(u => u.id === id), 10 [users, id] 11 ); 12} 13 14// Split context to reduce re-renders 15const UserDataContext = createContext<User | null>(null); 16const UserActionsContext = createContext<UserActions | null>(null); 17 18function UserProvider({ children }) { 19 const [user, setUser] = useState<User | null>(null); 20 21 // Actions never change 22 const actions = useMemo(() => ({ 23 login: async (credentials) => { /* ... */ }, 24 logout: () => setUser(null), 25 }), []); 26 27 return ( 28 <UserDataContext.Provider value={user}> 29 <UserActionsContext.Provider value={actions}> 30 {children} 31 </UserActionsContext.Provider> 32 </UserDataContext.Provider> 33 ); 34} 35 36// Stable callback with useEvent (React 18+) 37// Note: useEvent is experimental 38function Chat({ roomId }) { 39 const [message, setMessage] = useState(''); 40 41 // Always reads latest message without re-creating 42 const onSend = useEvent(() => { 43 sendMessage(roomId, message); 44 }); 45 46 return <SendButton onClick={onSend} />; 47}

Best Practices#

When to Use: ✓ Expensive computations ✓ Components with many children ✓ Callbacks passed to memoized children ✓ Context values ✓ Virtualized lists When to Skip: ✓ Simple/cheap operations ✓ Props that always change ✓ Components that rarely re-render ✓ Small component trees Debugging: ✓ Use React DevTools Profiler ✓ Add console.logs to track renders ✓ Use why-did-you-render in development ✓ Measure before optimizing Patterns: ✓ Move state down ✓ Lift content up ✓ Split contexts ✓ Use refs for callbacks

Conclusion#

React.memo, useMemo, and useCallback prevent unnecessary re-renders when used correctly. Profile first to identify actual performance issues, then apply memoization strategically. Over-optimization adds complexity without benefits. Focus on component composition patterns and virtualization for the biggest performance gains.

Share this article

Help spread the word about Bootspring