React: Otimização de desempenho: useMemo e useCallback
Última atualização: 2026-08-26
1. O que você vai aprender
- O
useMemoarmazena em cache o resultado de um cálculo - O
useCallbackarmazena em cache as referências às funções - O React.memo evita a reexibição desnecessária
- Quando usar, quando abusar
- Abordagens para a análise de desempenho
2. A história de uma página de filtro de pesquisa
A relação entre os três componentes da otimização do desempenho
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) Problema: Recálculo a cada entrada
Alice criou uma página de busca de produtos com 10.000 itens para filtrar:
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>
)
}
Problema: Sempre que a entrada do teclado (setSearch) é inserida, isso faz com que filter e sort sejam executados novamente — 10.000 registros precisam ser filtrados a cada vez, o que causa lentidão na interface.
(2) A solução useMemo
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>
)
}
Benefícios: A filtragem só é executada novamente quando a entrada muda para search ou sortBy; outras mudanças de estado (como a reexibição do componente pai) não acionam a filtragem, resultando em uma melhoria de desempenho de 10 a 100 vezes.
3. useMemo: Armazenamento em cache de resultados de cálculos
| Hook | O que é preciso lembrar | Valor de retorno | Quando as dependências mudam |
|---|---|---|---|
useMemo |
Resultado do cálculo | Valor armazenado em cache | Recalcular |
useCallback |
Referências de funções | Funções armazenadas em cache | Criação de novas funções |
React.memo |
Resultado renderizado de todo o componente | Ignorar nova renderização | Renderizar novamente quando os props mudarem |
(1) Sintaxe básica
const memoizedValue = useMemo(() => {
return Costly Computations()
}, [Dependency1, Dependency2])
| Seção | Descrição |
|---|---|
() => Computation |
Uma função que precisa ser armazenada em cache e retorna o resultado do cálculo |
[Dependencies] |
Recalcular quando as dependências mudarem |
memoizedValue |
Resultados em cache (retorna diretamente o resultado anterior se as dependências não tiverem mudado) |
(2) Cenários aplicáveis
// 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])
▶ Exemplo: Comparação de desempenho
// ============================================
// 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
4. useCallback: Lembrando-se das referências a funções
(1) Sintaxe básica
const memoizedFn = useCallback(() => {
doSomething(a, b)
}, [a, b])
useCallback(fn, deps) é equivalente a useMemo(() => fn, deps) — o que é armazenado é a própria função, e não o resultado da função.
(2) Por que o useCallback é necessário?
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: Evitando que componentes filhos sejam renderizados novamente
// ============================================
// 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. Quando usar e quando não usar
(1) Quando usar
| Cenário | Gancho | Motivo |
|---|---|---|
| Filtragem/Classificação de Big Data | useMemo |
Evitar percorrer grandes quantidades de dados a cada renderização |
| Cálculos complexos (Matemática/Formatação) | useMemo |
Não é preciso recalcular se o resultado for o mesmo |
| Passagem de propriedades para um componente filho do React.memo | useCallback |
Manutenção da estabilidade das referências às funções para que o React.memo funcione |
| Funções nas dependências do useEffect | useCallback |
Como evitar que o useEffect seja executado várias vezes devido a alterações nas referências da função |
(2) Situações em que não é necessário
// ❌ 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) O custo do abuso
// 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. Exemplo completo: Lista de mensagens em um aplicativo de bate-papo
// ============================================
// 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'
}
Resultado esperado: Uma lista de bate-papo com 100 mensagens. Ao clicar em “Aumentar contagem de mensagens não lidas”, a lista de mensagens não será atualizada (não haverá saída no console) devido a
useCallbackeReact.memo; apenas a contagem será alterada.
▶ Exemplo 2: o useCallback impede que o useEffect seja executado várias vezes
// ============================================
// 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>
)
}
▶ Exemplo 3: Utilização de useMemo para implementar cálculos de listas virtuais
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>
)
}
▶ Exemplo 4: Otimização de listas com React.memo + useCallback
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>
)
}
▶ Exemplo 5: Quando não usar useMemo — Comparação de antipadrões
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>
)
}
❓ Perguntas Frequentes
P:
useMemoeuseCallbacksempre melhoram o desempenho? R: Não necessariamente. Eles trazem sua própria sobrecarga — comparam a matrizdependenciesa cada renderização. Usá-los em excesso pode, na verdade, tornar o sistema mais lento. Recomendação oficial do React: comece escrevendo a versão não otimizada e só otimize quando perceber lentidão. Regra geral: use-os apenas para cálculos passados a um componente filho do React.memo ou aqueles com uma sobrecarga superior a 1 ms.
P: Qual é a diferença entre
useMemoe uma variável comum? R:useMemopossui “memória” — se as dependências não tiverem mudado, ela usa o valor armazenado em cache diretamente, sem reavaliar a expressão. Uma variável comum é reavaliada a cada renderização. No entanto,useMemoprecisa comparar a matriz de dependências durante a renderização, de modo que “cálculos simples ficam, na verdade, mais lentos comuseMemo”.
P: O React.memo é gratuito? R: Não, não é. O React.memo realiza uma comparação superficial (PrevProps vs. NextProps) toda vez que o componente pai é renderizado, e a comparação em si gera sobrecarga. Se as props do componente filho forem diferentes a cada vez (como um objeto inline
style={{ color: 'red' }}), a comparação superficial do React.memo sempre retornaráfalse, tornando o esforço um desperdício.
P: Quando se deve usar
useCallbackpara preservar uma referência a uma função? R: Você só precisa usá-lo quando ambas as condições a seguir forem atendidas: ① A função é passada como um prop para um componente filho do React.memo; ② A função está no array de dependências de um hookuseEffect. Se nenhuma dessas condições se aplicar, você pode simplesmente escrever a função diretamente;useCallbacké desnecessário.
P: Qual é a diferença entre
useMemoeuseCallback? R:useMemo(() => value, deps)armazena em cache o resultado (valor) de um cálculo, enquantouseCallback(() => fn, deps)armazena em cache uma referência a uma função. Na verdade,useCallback(fn, deps)é equivalente auseMemo(() => fn, deps). UseuseCallbackquando precisar armazenar em cache uma função passada para um componente filho; useuseMemoquando precisar armazenar em cache o resultado de um cálculo.
📖 Resumo
useMemoArmazenar em cache os resultados dos cálculos; se as dependências permanecerem inalteradas, não recalcularuseCallbackArmazenar em cache referências a funções para evitar que componentes filhos sejam renderizados novamente em conjunto com o React.memoReact.memoRealizar uma comparação superficial dos props; se os props não tiverem mudado, pular a renderização do componente filho- Não otimize muito cedo — escreva primeiro um código que funcione e só o otimize quando ele começar a ficar lento.
- Identificando gargalos de desempenho com o Profiler do React DevTools
📝 Exercícios
- Problema Básico (Dificuldade ⭐): Crie um componente
PrimeCalculatorque receba um número n como entrada e calcule o enésimo número primo. UseuseMemopara armazenar o resultado em cache, de modo que ele não seja recalculado toda vez que um novo valor for inserido. - Exercício avançado (Dificuldade ⭐⭐): Crie um componente
ProductTableque contenha 500 registros de produtos e permita a filtragem por categoria e a ordenação por preço. UseuseMemopara otimizar a lógica de filtragem e ordenação, e useuseCallbackem combinação comReact.memopara otimizar a renderização da lista. - Desafio (Dificuldade: ⭐⭐⭐): Crie um componente
DataDashboardque agregue 10.000 registros de pedidos em três dimensões diferentes (tempo, região e categoria) e useuseMemopara implementar o armazenamento em cache hierárquico (de modo que apenas as camadas relevantes sejam recalculadas quando as dimensões mudarem).