React: Performance Optimization: useMemo and useCallback
Last updated: 2026-08-26
1. What You'll Learn
- useMemo caches the result of a calculation
- useCallback caches função references
- React.memo prevents unnecessary re-rendering
- When to Use It, When to Abuse It
- Approaches to Performance Analysis
2. The Story of a Search Filter Page
The Relationship Among the Three Components of Performance Optimization
flowchart TD
A[Parent Component Rendering] --> B{Props It's changed?}
B -->|Yes| C[React.memo Intercept?]
B -->|No| D[Skip subcomponents ✅]
C -->|A rough comparison is roughly equal| D
C -->|Shallow Comparison Inequality| E{Function/The object is new.?}
E -->|Yes| F[useCallback/useMemo]
F --> G[Stable Citation → memo Effective ✅]
E -->|No| D
style F fill:#e8f5e9,stroke:#2e7d32
style D fill:#c8e6c9,stroke:#2e7d32
style G fill:#c8e6c9,stroke:#2e7d32
(1) Pain Point: Recalculation with Every Input
Alice created a product search page with 10,000 products to filter through:
function ShopPage() {
const [products] = useState(generate10000Products())
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState('name')
// ❌ Question:Re-filter after each input 10,000 Items
const filtered = products
.filter(p => p.name.includes(search))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1)
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">By Name</option>
<option value="price">By Price</option>
</select>
<ProductList products={filtered} />
</div>
)
}
Issue: Every time the keyboard input (setSearch) is entered, it triggers filter and sort to run again—10,000 records have to be filtered each time, causing the interface to lag.
(2) The useMemo Solution
function ShopPage() {
const [products] = useState(generate10000Products())
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState('name')
// ✅ useMemo:Only at search or sortBy Recalculate only when there is a change
const filtered = useMemo(() => {
console.log('Refilter...(Only when the search terms or sorting criteria change)')
return products
.filter(p => p.name.includes(search))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1)
}, [search, sortBy, products])
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">By Name</option>
<option value="price">By Price</option>
</select>
<ProductList products={filtered} />
</div>
)
}
Benefits: Filtering is only re-run when the input changes to either search or sortBy; other state changes (such as the parent component being re-rendered) do not trigger filtering, resulting in a 10- to 100-fold performance improvement.
3. useMemo: Caching Calculation Results
| Hook | What to remember | Return value | When dependencies change |
|---|---|---|---|
useMemo |
Calculation Result | Cached Value | Recalculate |
useCallback |
Function References | Cached Functions | Creating New Functions |
React.memo |
Rendered result of the entire component | Skip re-rendering | Re-render when props change |
(1) Basic Syntax
const memoizedValue = useMemo(() => {
return Costly Computations()
}, [Dependency1, Dependency2])
| Section | Description |
|---|---|
() => Computation |
A function that needs to be cached and returns the result of the calculation |
[Dependencies] |
Recalculate when dependencies change |
memoizedValue |
Cached results (returns the previous result directly if dependencies haven't changed) |
(2) Applicable Scenarios
// Scene 1:Data Conversion(Filter、Sort、Grouping)
const activeUsers = useMemo(
() => users.filter(u => u.isActive).sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
// Scene 2:Format(Numbers、Date、String Concatenation)
const formattedPrice = useMemo(
() => new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(price),
[price]
)
// Scene 3:Derived State(Calculating New Data from Existing Data)
const statistics = useMemo(() => ({
total: orders.length,
paid: orders.filter(o => o.status === 'paid').length,
amount: orders.reduce((sum, o) => sum + o.amount, 0),
avgAmount: orders.length ? orders.reduce((sum, o) => sum + o.amount, 0) / orders.length : 0
}), [orders])
▶ Example: Performance Comparison
Output:
...
// ============================================
// Example:useMemo vs no useMemo Performance Comparison
// ============================================
function ExpensiveCalc({ num, unrelated }) {
// ❌ None useMemo:Executed on every render
const slowResult1 = slowFibonacci(num)
// ✅ With useMemo: num If it hasn't changed, don't recalculate it.
const slowResult2 = useMemo(() => slowFibonacci(num), [num])
return (
<div>
<p>num={num} The Fibonacci sequence</p>
<p>Without useMemo: {slowResult1}(Rendering calculations each time)</p>
<p>With useMemo: {slowResult2}(Only at num Calculate when changes occur)</p>
<p>Unrelated Status:{unrelated}(Click the button to change this value)</p>
</div>
)
}
function slowFibonacci(n) {
if (n <= 1) return n
let a = 0, b = 1, c
for (let i = 2; i <= n; i++) { c = a + b; a = b; b = c }
console.log('fibonacci Calculation!n=' + n)
return b
}
// Click to Make a Difference unrelated → without useMemo recalculates,with useMemo skips
Output:
Fibonacci(n): without useMemo recalculates every render, with useMemo skips when only unrelated state changes. Console: "fibonacci calculated! n=X"
4. useCallback: Remembering Function References
(1) Basic Syntax
const memoizedFn = useCallback(() => {
doSomething(a, b)
}, [a, b])
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps) — what is stored is the function itself, not the result of the function.
(2) Why is useCallback needed?
function Parent() {
const [count, setCount] = useState(0)
// ❌ Every time Parent Rendering,handleClick These are all new functions
// resulting in Child 's React.memo Failure
const handleClick = () => {
console.log('Clicked')
}
// ✅ useCallback:handleClick Citation unchanged
// Child 's React.memo Works properly(Skip Re-rendering)
const handleClick = useCallback(() => {
console.log('Clicked')
}, []) // Empty Dependency:This function will never change.
}
(3) React.memo: Preventing Child Components from Rendering Again
// ============================================
// Example:useCallback + React.memo For use in combination with
// ============================================
// ---- Child component:use React.memo Package ----
const ExpensiveList = React.memo(function ExpensiveList({ items, onItemClick }) {
console.log('ExpensiveList Render Again!')
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => onItemClick(item.id)}>Select</button>
</li>
))}
</ul>
)
})
// ---- Parent Component ----
function App() {
const [count, setCount] = useState(0)
const [items] = useState([
{ id: 1, name: 'Project A' },
{ id: 2, name: 'Project B' },
{ id: 3, name: 'Project C' }
])
// ❌ Every time App Rendering,All function references have changed → React.memo Failure
const handleClickBad = (id) => console.log('Select:', id)
// ✅ Function references are immutable → React.memo Effective
const handleClick = useCallback((id) => {
console.log('Select:', id)
}, [])
return (
<div>
<p>Counter:{count}</p>
<button onClick={() => setCount(c => c + 1)}>Change Count</button>
{/* Tap the button to change the count → No impact ExpensiveList Rendering of */}
<ExpensiveList items={items} onItemClick={handleClick} />
</div>
)
}
// When clicking "Change Count" :
// ❌ Without useCallback → ExpensiveList Render again
// ✅ has useCallback → ExpensiveList Skip rendering(Performance Improvements)
5. When to Use It, and When Not to Use It
(1) When to Use It
| Scenario | Hook | Reason |
|---|---|---|
| Big Data Filtering/Sorting | useMemo |
Avoid traversing large amounts of data on every render |
| Complex Calculations (Math/Formatting) | useMemo |
No need to recalculate if the result remains the same |
| Passing props to a React.memo child component | useCallback |
Keeping function references stable so that React.memo works |
| Functions in useEffect Dependencies | useCallback |
Preventing useEffect from Running Multiple Times Due to Changes in Function References |
(2) Situations Where It Is Not Needed
// ❌ Not necessary:Simple Calculations
const doubled = useMemo(() => count * 2, [count])
// ✅ Write it directly:count * 2 It's already pretty fast as it is
const doubled = count * 2
// ❌ Not necessary:Handle events without passing them to child components
const handleClick = useCallback(() => {
console.log('Click')
}, [])
// ✅ Just write it down.
const handleClick = () => console.log('Click')
// ❌ Not necessary:The child component isn't being used. React.memo
// useCallback It's been packaged, but no one is buying it."Stable Citation"
(3) The Cost of Abuse
// useMemo and useCallback It involves expenses(Relies heavily on arrays)
// Abuse = Introducing Additional Comparison Logic,Slower than if it weren't optimized
// Rule of thumb:
// 1. First, let's write the version without optimization.
// 2. Feeling some lag(fps < 60)Use again
// 3. use React DevTools Profiler Measurement
6. Complete Example: Message List in a Chat App
// ============================================
// Complete Example:Chat Message List(Comprehensive Performance Optimization)
// ============================================
import { useState, useMemo, useCallback, memo } from 'react'
// ---- News Item(React.memo + useCallback) ----
const MessageItem = memo(function MessageItem({ message, onReact, onReply }) {
console.log(`Rendering Message:${message.id}`)
return (
<div style={{
padding: '12px',
margin: '8px 0',
backgroundColor: message.isMine ? '#e6f7ff' : '#f5f5f5',
borderRadius: '8px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<strong>{message.author}</strong>
<span style={{ color: '#999', fontSize: '12px' }}>
{message.time}
</span>
</div>
<p style={{ margin: '8px 0' }}>{message.text}</p>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => onReact(message.id)} style={smallBtn}>
👍 {message.reactions}
</button>
<button onClick={() => onReply(message.id)} style={smallBtn}>
💬 Reply
</button>
</div>
</div>
)
})
// ---- Main Component ----
function ChatApp() {
const [messages, setMessages] = useState(generateMessages(100))
const [filter, setFilter] = useState('All')
const [unreadCount, setUnreadCount] = useState(5)
// useMemo:Filter Messages(100 Filter by row)
const filteredMessages = useMemo(
() => filter === 'All'
? messages
: messages.filter(m => filter === 'My' ? m.isMine : !m.isMine),
[messages, filter]
)
// useCallback:Keep References Consistent,let MessageItem 's memo Effective
const handleReact = useCallback((id) => {
setMessages(prev => prev.map(m =>
m.id === id ? { ...m, reactions: m.reactions + 1 } : m
))
}, [])
const handleReply = useCallback((id) => {
console.log('Reply to a message:', id)
}, [])
// Calculate Statistical Information
const stats = useMemo(() => ({
total: messages.length,
mine: messages.filter(m => m.isMine).length,
totalReactions: messages.reduce((s, m) => s + m.reactions, 0)
}), [messages])
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2>💬 Chat({unreadCount} Unread posts)</h2>
<button onClick={() => setUnreadCount(c => c + 1)}>
Increase the unread count(Test Performance)
</button>
</div>
{/* Statistics(useMemo) */}
<div style={{ display: 'flex', gap: '16px', marginBottom: '12px', fontSize: '13px', color: '#666' }}>
<span>Total {stats.total} items</span>
<span>My {stats.mine} items</span>
<span>👍 {stats.totalReactions}</span>
</div>
{/* Filter Button */}
<div style={{ marginBottom: '12px' }}>
{['All', 'My', 'Other people's'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
style={{
padding: '4px 12px', margin: '0 4px',
backgroundColor: filter === f ? '#1890ff' : '#f0f0f0',
color: filter === f ? 'white' : '#333',
border: 'none', borderRadius: '4px', cursor: 'pointer'
}}
>
{f}
</button>
))}
</div>
{/* News List */}
<div style={{ maxHeight: '500px', overflow: 'auto' }}>
{filteredMessages.map(msg => (
<MessageItem
key={msg.id}
message={msg}
onReact={handleReact} // ✅ Stable citation
onReply={handleReply} // ✅ Stable citation
/>
))}
</div>
</div>
)
}
function generateMessages(count) {
const names = ['Alice', 'Bob', 'Charlie', 'Diana']
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
author: names[i % 4],
text: `This is the ${i + 1} The content of the message`,
time: `${String(10 + i % 8).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}`,
isMine: i % 3 === 0,
reactions: Math.floor(Math.random() * 5)
}))
}
const smallBtn = {
padding: '2px 8px', fontSize: '12px',
border: '1px solid #d9d9d9', borderRadius: '4px',
backgroundColor: 'white', cursor: 'pointer'
}
Expected Output: A chat list with 100 messages. When you click "Increase Unread Count," the message list will not re-render (no output in the console) due to
useCallbackandReact.memo; only the count will change.
▶ Example 2: useCallback prevents useEffect from running multiple times
Output:
... ('fibonacci Calculation!n=' + n)
// ============================================
// Example:useCallback In conjunction with useEffect Avoiding Infinite Loops
// Features:Display useCallback in useEffect The Key Role of Dependencies
// ============================================
import { useState, useEffect, useCallback } from 'react'
function SearchComponent() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [page, setPage] = useState(1)
// ❌ Question:Each render is a new function,As useEffect Dependencies can cause an infinite loop
// const fetchResults = async () => {
// const res = await fetch(`/api/search?q=${query}&page=${page}`)
// const data = await res.json()
// setResults(data)
// }
// ✅ Resolve:useCallback Stable Function References
const fetchResults = useCallback(async () => {
// Simulation API Request
const mockData = [
{ id: 1, title: `Results ${query} - Page ${page}` },
{ id: 2, title: `Related Articles About ${query}` },
{ id: 3, title: `${query} Best Practices` }
]
setResults(mockData)
}, [query, page]) // query or page Create a new function only when there is a change
// Dependency fetchResults(Stable Citation,It won't go into an infinite loop)
useEffect(() => {
if (query.trim()) {
fetchResults()
}
}, [fetchResults]) // fetchResults Citation unchanged → useEffect Will not be executed again
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>🔍 Search(useCallback Demo)</h2>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Enter a search term..."
style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}
/>
<div style={{ marginTop: '8px', display: 'flex', gap: '8px', alignItems: 'center' }}>
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page <= 1}>Previous Page</button>
<span>Page {page} </span>
<button onClick={() => setPage(p => p + 1)}>Next Page</button>
</div>
<ul style={{ marginTop: '12px', listStyle: 'none', padding: 0 }}>
{results.map(r => (
<li key={r.id} style={{ padding: '8px', borderBottom: '1px solid #f0f0f0' }}>
{r.title}
</li>
))}
</ul>
{!query && <p style={{ color: '#999', textAlign: 'center' }}>Enter a keyword to start your search</p>}
</div>
)
}
Output:
Search box + pagination. Type "React" → 3 results ("Results React - Page 1", etc.). useCallback prevents infinite useEffect loop. Prev/Next buttons.
▶ Example 3: Using useMemo to Implement Virtual List Calculations
Output:
Subheading: "🔍 Search(useCallback Demo)". Displays: "🔍 Search(useCallback Demo)". State: query (setter: setQuery), results (setter: setResults), page (setter: setPage). Button: setPage(p => p + 1)}>Next Page. Input: Enter a search term.... List: {r.title}. useEffect manages side effects. useCallback memoizes handler. Async data fetching/loading states
function VirtualList({ items, itemHeight = 40, visibleHeight = 400 }) {
const [scrollTop, setScrollTop] = useState(0)
const visibleCount = Math.ceil(visibleHeight / itemHeight)
const totalHeight = useMemo(() => items.length * itemHeight, [items.length, itemHeight])
const { startIndex, endIndex, offsetY } = useMemo(() => {
const start = Math.floor(scrollTop / itemHeight)
const end = Math.min(start + visibleCount + 1, items.length)
return { startIndex: start, endIndex: end, offsetY: start * itemHeight }
}, [scrollTop, itemHeight, visibleCount, items.length])
const visibleItems = useMemo(() => items.slice(startIndex, endIndex), [items, startIndex, endIndex])
return (
<div onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
style={{ height: visibleHeight, overflowY: 'auto', border: '1px solid #ddd', borderRadius: 4 }}>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ position: 'absolute', top: offsetY, width: '100%' }}>
{visibleItems.map((item, i) => (
<div key={startIndex + i} style={{ height: itemHeight, padding: '0 12px', display: 'flex', alignItems: 'center', borderBottom: '1px solid #f0f0f0' }}>
Row {startIndex + i}: {item}
</div>
))}
</div>
</div>
</div>
)
}
Output:
Virtual scroll: renders only visible rows (~10 of 1000+). Scroll → rows swap. Total height = items × 40px. useMemo computes visible range.
▶ Example 4: Optimizing Lists with React.memo + useCallback
Output:
Displays: "items.slice(startIndex, endIndex), [items, startIndex, endIn". State: scrollTop (setter: setScrollTop). useMemo optimizes computation
const ListItem = React.memo(function ListItem({ name, price, onRemove }) {
console.log(`Rendering: ${name}`)
return (
<div style={{ display: 'flex', justifyContent: 'space-between', padding: 8, borderBottom: '1px solid #f0f0f0' }}>
<span>{name} - ${price}</span>
<button onClick={() => onRemove(name)} style={{ color: '#ff4d4f', border: 'none', cursor: 'pointer' }}>Remove</button>
</div>
)
})
function ProductList() {
const [products, setProducts] = useState([
{ name: 'Keyboard', price: 79 },
{ name: 'Mouse', price: 49 },
{ name: 'Monitor', price: 399 },
{ name: 'Headphones', price: 129 },
])
const [filter, setFilter] = useState('')
const handleRemove = useCallback((name) => {
setProducts(prev => prev.filter(p => p.name !== name))
}, [])
const filtered = useMemo(() => products.filter(p => p.name.toLowerCase().includes(filter.toLowerCase())), [products, filter])
return (
<div style={{ maxWidth: 400, margin: '0 auto' }}>
<input value={filter} onChange={e => setFilter(e.target.value)} placeholder="Filter..." style={{ width: '100%', padding: 8, marginBottom: 8, borderRadius: 4 }} />
{filtered.map(p => <ListItem key={p.name} name={p.name} price={p.price} onRemove={handleRemove} />)}
</div>
)
}
Output:
Product list (Keyboard $79, Mouse $49, Monitor $399, Headphones $129) with filter. "Remove" deletes item. React.memo + useCallback = only changed items re-render.
▶ Example 5: When Not to Use useMemo—Anti-Pattern Comparison
Output:
Rendering: ...
function BadExample({ count }) {
// ❌ Simple calculations are not required useMemo——Create memo Expenses > The Calculation Itself
const doubled = useMemo(() => count * 2, [count])
// ✅ Direct Calculation
const doubledOk = count * 2
// ❌ Each render creates a new object as useMemo Dependency
const config = useMemo(() => ({ count }), [count])
const badResult = useMemo(() => config.count * 3, [config]) // config A new reference each time!
// ✅ Using Primitive Types Directly as Dependencies
const goodResult = useMemo(() => count * 3, [count])
return <p>Doubled: {doubledOk}, Tripled: {goodResult}</p>
}
function GoodExample({ items, query }) {
// ✅ Filter/Sorting large amounts of data useMemo
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
).sort((a, b) => a.name.localeCompare(b.name))
}, [items, query])
// ✅ Complex objects passed to child components use useMemo
const chartData = useMemo(() => ({
labels: filtered.map(i => i.name),
values: filtered.map(i => i.price),
}), [filtered])
return (
<div>
<p>Found: {filtered.length} items</p>
<Chart data={chartData} />
</div>
)
}
Output:
❌ useMemo for count*2 (overhead > calculation). ❌ New object as dep breaks memo. ✅ useMemo for large filter/sort + derived chart data objects.
❓ FAQ
useMemo and useCallback always improve performance?dependencies array on every render. Overusing them can actually slow things down. React’s official recommendation: Start by writing the unoptimized version, and only optimize when you notice lag. Rule of thumb: Use them only for calculations passed to a React.memo child component or those with an overhead greater than 1 ms.useMemo and a regular variable?useMemo has "memory"—if the dependencies haven’t changed, it uses the cached value directly without re-evaluating the expression. A regular variable is re-evaluated on every render. However, useMemo needs to compare the array of dependencies during rendering, so "simple calculations are actually slower with useMemo."useCallback to preserve a function reference?useEffect hook. If neither of these conditions applies, you can simply write the function directly; useCallback is unnecessary.useMemo and useCallback?useMemo(() => value, deps) caches the result (value) of a calculation, while useCallback(() => fn, deps) caches a function reference. In fact, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Use useCallback when you need to cache a function passed to a child component; use useMemo when you need to cache a computation result.📖 Summary
useMemoCache calculation results; if dependencies remain unchanged, do not recalculateuseCallbackCache function references to prevent child components from re-rendering in conjunction with React.memoReact.memoPerform a shallow comparison of props; if the props haven't changed, skip rendering the child component- Don't optimize too early—write code that works first, and optimize it only when it starts to lag.
- Identifying Performance Bottlenecks with the React DevTools Profiler
📝 Exercises
- Basic Problem (Difficulty ⭐): Create a
PrimeCalculatorcomponent that takes a number n as input and calculates the nth prime number. UseuseMemoto cache the result so it isn't recalculated every time a new value is entered. - Advanced Exercise (Difficulty ⭐⭐): Create a
ProductTablecomponent that contains 500 product records and supports filtering by category and sorting by price. UseuseMemoto optimize the filtering and sorting logic, and useuseCallbackcombined withReact.memoto optimize list rendering. - Challenge (Difficulty: ⭐⭐⭐): Create a
DataDashboardcomponent that aggregates 10,000 order records across three different dimensions (time, region, and category), and useuseMemoto implement hierarchical caching (so that only the relevant layers are recalculated when the dimensions change).