Performance Optimization: Guide to Using memo, useMemo, and useCallback
React performance optimization is crucial for building fast, responsive applications. As your application grows, you'll encounter performance bottlenecks that can impact user experience. Understanding when and how to use React's optimization tools—memo, useMemo, and useCallback—is essential for creating efficient applications. These tools help prevent unnecessary re-renders and expensive recalculations, but they must be used correctly to avoid premature optimization or performance degradation. In this guide, you'll learn the practical differences between these optimization techniques, when to use each one, and common pitfalls to avoid. We'll explore real-world examples from building ContentQR, where performance optimization played a critical role in handling complex QR code generation and content management workflows. By the end, you'll have a clear understanding of how to optimize your React components effectively.
Understanding React Re-renders
Before diving into optimization techniques, you need to understand when React components re-render:
Re-render Triggers:
- State changes (
useState,useReducer) - Props changes
- Parent component re-renders
- Context value changes
Important: Not all re-renders are bad. React is fast, and most re-renders are necessary and efficient. Only optimize when you have a performance problem.
React.memo: Preventing Component Re-renders
React.memo is a higher-order component that memoizes the result of a component. It only re-renders if its props have changed.
Basic Usage
import { memo } from 'react';
interface UserCardProps {
name: string;
email: string;
avatar: string;
}
const UserCard = memo(function UserCard({ name, email, avatar }: UserCardProps) {
console.log('UserCard rendered');
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{email}</p>
</div>
);
});
export default UserCard;
When to Use memo
✅ Good Use Cases:
- Expensive components that render frequently
- Components that receive stable props
- List items in large lists
❌ Avoid When:
- Props change frequently
- Component is cheap to render
- Premature optimization
Custom Comparison Function
You can provide a custom comparison function:
const UserCard = memo(
UserCard,
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
// Return false if props are different (re-render)
return prevProps.id === nextProps.id &&
prevProps.name === nextProps.name;
}
);
Real-World Example: QR Code List
In ContentQR, we use memo for QR code list items:
import { memo } from 'react';
interface QRCodeItemProps {
id: string;
title: string;
url: string;
createdAt: string;
onClick: (id: string) => void;
}
const QRCodeItem = memo(function QRCodeItem({
id,
title,
url,
createdAt,
onClick
}: QRCodeItemProps) {
return (
<div
className="qr-code-item"
onClick={() => onClick(id)}
>
<h3>{title}</h3>
<p>{url}</p>
<span>{new Date(createdAt).toLocaleDateString()}</span>
</div>
);
}, (prevProps, nextProps) => {
// Only re-render if id or title changes
return prevProps.id === nextProps.id &&
prevProps.title === nextProps.title;
});
export default QRCodeItem;
useMemo: Memoizing Expensive Calculations
useMemo memoizes the result of an expensive calculation and only recalculates when dependencies change.
Basic Usage
import { useMemo } from 'react';
function ExpensiveComponent({ items, filter }: { items: Item[]; filter: string }) {
// Expensive calculation - only runs when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<div>
{filteredItems.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
When to Use useMemo
✅ Good Use Cases:
- Expensive calculations (filtering, sorting, transformations)
- Creating objects/arrays that are passed as props
- Preventing unnecessary recalculations
❌ Avoid When:
- Simple calculations
- Premature optimization
- Dependencies change frequently
Real-World Example: Content Processing
In ContentQR, we use useMemo for processing markdown content:
import { useMemo } from 'react';
import { marked } from 'marked';
interface ContentPreviewProps {
markdown: string;
options: ProcessingOptions;
}
function ContentPreview({ markdown, options }: ContentPreviewProps) {
// Expensive: Parsing markdown and applying transformations
const processedContent = useMemo(() => {
console.log('Processing markdown...');
const parsed = marked.parse(markdown);
// Apply custom transformations based on options
if (options.highlightCode) {
// Code highlighting logic
}
if (options.processLinks) {
// Link processing logic
}
return parsed;
}, [markdown, options.highlightCode, options.processLinks]);
return (
<div
className="content-preview"
dangerouslySetInnerHTML={{ __html: processedContent }}
/>
);
}
Common Pitfall: Object Dependencies
Be careful with object dependencies:
// ❌ Bad: options object recreated every render
const processed = useMemo(() => {
return processData(data, options);
}, [data, options]); // options is new object every time
// ✅ Good: Use specific properties
const processed = useMemo(() => {
return processData(data, options.highlightCode, options.processLinks);
}, [data, options.highlightCode, options.processLinks]);
useCallback: Memoizing Functions
useCallback memoizes a function and returns the same function reference unless dependencies change.
Basic Usage
import { useCallback, useState } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
// Memoized callback - same function reference unless count changes
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // Empty deps: function never changes
return <ChildComponent onClick={handleClick} />;
}
const ChildComponent = memo(function ChildComponent({
onClick
}: {
onClick: () => void
}) {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Increment</button>;
});
When to Use useCallback
✅ Good Use Cases:
- Functions passed to memoized components
- Functions in dependency arrays (
useEffect,useMemo) - Event handlers in frequently re-rendering components
❌ Avoid When:
- Functions don't need stable references
- Premature optimization
- Simple functions that don't cause issues
Real-World Example: QR Code Generation
In ContentQR, we use useCallback for QR code generation handlers:
import { useCallback, useState } from 'react';
import { QRCodeItem } from './QRCodeItem';
interface QRCodeListProps {
qrCodes: QRCode[];
onGenerate: (content: string) => Promise<void>;
}
function QRCodeList({ qrCodes, onGenerate }: QRCodeListProps) {
const [isGenerating, setIsGenerating] = useState(false);
// Memoized handler - stable reference for memoized child components
const handleGenerate = useCallback(async (content: string) => {
setIsGenerating(true);
try {
await onGenerate(content);
} finally {
setIsGenerating(false);
}
}, [onGenerate]);
return (
<div>
{qrCodes.map(qrCode => (
<QRCodeItem
key={qrCode.id}
qrCode={qrCode}
onGenerate={handleGenerate}
/>
))}
</div>
);
}
// QRCodeItem is memoized, so it needs stable function references
const QRCodeItem = memo(function QRCodeItem({
qrCode,
onGenerate
}: QRCodeItemProps) {
return (
<div>
<h3>{qrCode.title}</h3>
<button onClick={() => onGenerate(qrCode.content)}>
Regenerate
</button>
</div>
);
});
Common Pitfall: Unnecessary Dependencies
// ❌ Bad: Including unnecessary dependencies
const handleClick = useCallback(() => {
console.log('Clicked');
setCount(count + 1); // Using count in closure
}, [count]); // Recreates function when count changes
// ✅ Good: Use functional update
const handleClick = useCallback(() => {
console.log('Clicked');
setCount(c => c + 1); // Functional update
}, []); // Stable function reference
Combining Optimization Techniques
In real applications, you'll often combine these techniques:
import { memo, useMemo, useCallback, useState } from 'react';
interface FilteredListProps {
items: Item[];
onItemClick: (id: string) => void;
}
function FilteredList({ items, onItemClick }: FilteredListProps) {
const [filter, setFilter] = useState('');
// Memoized filtered list
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoized click handler
const handleItemClick = useCallback((id: string) => {
onItemClick(id);
}, [onItemClick]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
{filteredItems.map(item => (
<ListItem
key={item.id}
item={item}
onClick={handleItemClick}
/>
))}
</div>
);
}
// Memoized list item
const ListItem = memo(function ListItem({
item,
onClick
}: {
item: Item;
onClick: (id: string) => void;
}) {
return (
<div onClick={() => onClick(item.id)}>
{item.name}
</div>
);
});
Performance Measurement
Before optimizing, measure performance:
import { Profiler } from 'react';
function App() {
const onRenderCallback = (
id: string,
phase: 'mount' | 'update',
actualDuration: number
) => {
console.log(`${id} ${phase}: ${actualDuration}ms`);
};
return (
<Profiler id="App" onRender={onRenderCallback}>
<YourComponent />
</Profiler>
);
}
React DevTools Profiler
Use React DevTools Profiler to identify performance bottlenecks:
- Record a performance profile
- Identify components with long render times
- Check for unnecessary re-renders
- Optimize based on actual data
Best Practices
1. Measure First, Optimize Later
Don't optimize prematurely. Measure performance first:
// ❌ Premature optimization
const Component = memo(function Component({ data }) {
const processed = useMemo(() => data.map(x => x * 2), [data]);
return <div>{processed}</div>;
});
// ✅ Measure first, then optimize if needed
function Component({ data }) {
const processed = data.map(x => x * 2);
return <div>{processed}</div>;
}
2. Use memo for Expensive Components
Only memoize components that are expensive to render:
// ✅ Good: Expensive component
const ExpensiveChart = memo(function ExpensiveChart({ data }) {
// Complex chart rendering
return <ComplexChart data={data} />;
});
// ❌ Unnecessary: Simple component
const SimpleText = memo(function SimpleText({ text }) {
return <p>{text}</p>;
});
3. Be Careful with Dependencies
Always include all dependencies:
// ❌ Missing dependency
const filtered = useMemo(() => {
return items.filter(item => item.category === category);
}, [items]); // Missing category
// ✅ Correct dependencies
const filtered = useMemo(() => {
return items.filter(item => item.category === category);
}, [items, category]);
4. Consider Context Performance
When using Context, consider splitting contexts:
// ❌ Bad: Single context with all values
const AppContext = createContext({ user, theme, settings });
// ✅ Good: Split contexts
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
const SettingsContext = createContext(settings);
Common Mistakes to Avoid
1. Overusing memo
// ❌ Unnecessary memo
const Button = memo(function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
});
2. Incorrect Dependencies
// ❌ Wrong dependencies
const result = useMemo(() => {
return process(data, options);
}, [data]); // Missing options
// ✅ Correct dependencies
const result = useMemo(() => {
return process(data, options);
}, [data, options]);
3. Creating Functions in Render
// ❌ Function recreated every render
function Component({ items }) {
const handleClick = (id) => {
// Handle click
};
return items.map(item => (
<Item key={item.id} onClick={handleClick} />
));
}
// ✅ Memoized function
function Component({ items }) {
const handleClick = useCallback((id) => {
// Handle click
}, []);
return items.map(item => (
<Item key={item.id} onClick={handleClick} />
));
}
Conclusion
React performance optimization with memo, useMemo, and useCallback is powerful when used correctly. The key is understanding when each tool is appropriate and measuring performance before optimizing. In ContentQR, these optimizations helped us handle complex QR code generation workflows efficiently, but we only applied them after identifying actual performance bottlenecks through profiling.
Key Takeaways:
- Use
memoto prevent component re-renders when props haven't changed - Use
useMemofor expensive calculations that depend on specific values - Use
useCallbackto maintain stable function references for memoized components - Always measure performance before optimizing—don't optimize prematurely
- Include all dependencies in dependency arrays to avoid bugs
- Combine these techniques for maximum performance benefits in complex components
Next Steps:
- Profile your React application using React DevTools Profiler
- Identify components with performance issues
- Apply
memo,useMemo, oruseCallbackbased on actual bottlenecks - Test optimizations to ensure they improve performance
- Consider using Next.js App Router for built-in performance optimizations
- Learn about Server Components vs Client Components to reduce client-side bundle size
Related Posts
ContentQR Full-Stack Architecture Evolution: From Monolith to Modular Design
Learn how to evolve your architecture from monolith to modular design. Practical insights and lessons learned from real-world experience.
Advanced Type Handling: Generics and Utility Types Usage Tips
Master advanced TypeScript type handling with generics and utility types. Learn practical tips and patterns for complex type scenarios.
QR Code Analytics: Track Your QR Code Performance
Learn how to track and analyze your QR code performance using analytics. Measure scans, locations, and engagement metrics.