React: Performance Optimization: useMemo and useCallback
Last updated: 2026-10-26
1. What You'll Learn
- useMemo caches the result of a calculation
- useCallback caches function 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])
Output:
...
▶ Example: Performance Comparison
// ============================================
// 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
▶ Try it Yourself5. 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
▶ Expand runnable example (112 lines)
Output:
... ('fibonacci Calculation!n=' + n)
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
▶ Try it YourselfOutput:
Search box + pagination. Type "React" → 3 results ("Results React - Page 1", etc.). useCallback prevents infinite useEffect loop. Prev/Next buttons.
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
▶ Example 3: Using useMemo to Implement Virtual List Calculations
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.
Output:
Displays: "items.slice(startIndex, endIndex), [items, startIndex, endIn". State: scrollTop (setter: setScrollTop). useMemo optimizes computation
▶ Example 4: Optimizing Lists with React.memo + useCallback
▶ Try it YourselfOutput:
Product list (Keyboard $79, Mouse $49, Monitor $399, Headphones $129) with filter. "Remove" deletes item. React.memo + useCallback = only changed items re-render.
Output:
Rendering: ...
▶ Example 5: When Not to Use useMemo—Anti-Pattern Comparison
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).