React: Otimização de desempenho: useMemo e useCallback

Última atualização: 2026-08-26

1. O que você vai aprender



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

100%
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:

JSX
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>
  )
}
▶ Experimente

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

JSX
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>
  )
}
▶ Experimente

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

JSX
const memoizedValue = useMemo(() => {
  return Costly Computations()
}, [Dependency1, Dependency2])
▶ Experimente
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

JSX
// 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])
▶ Experimente

▶ Exemplo: Comparação de desempenho

JSX
// ============================================
// 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
▶ Experimente

4. useCallback: Lembrando-se das referências a funções

(1) Sintaxe básica

JSX
const memoizedFn = useCallback(() => {
  doSomething(a, b)
}, [a, b])
▶ Experimente

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?

JSX
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.
}
▶ Experimente

(3) React.memo: Evitando que componentes filhos sejam renderizados novamente

JSX
// ============================================
// 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)
▶ Experimente

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

JSX
// ❌ 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"
▶ Experimente

(3) O custo do abuso

JSX
// 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
▶ Experimente

6. Exemplo completo: Lista de mensagens em um aplicativo de bate-papo

JSX
// ============================================
// 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 useCallback e React.memo; apenas a contagem será alterada.


▶ Exemplo 2: o useCallback impede que o useEffect seja executado várias vezes

JSX 📖 Somente leitura
// ============================================
// 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>
  )
}
43 linhas de lógica (limite de 40, somente leitura)

▶ Exemplo 3: Utilização de useMemo para implementar cálculos de listas virtuais

JSX
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>
  )
}
▶ Experimente

▶ Exemplo 4: Otimização de listas com React.memo + useCallback

JSX
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>
  )
}
▶ Experimente

▶ Exemplo 5: Quando não usar useMemo — Comparação de antipadrões

JSX
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>
  )
}
▶ Experimente

❓ Perguntas Frequentes

P: useMemo e useCallback sempre melhoram o desempenho? R: Não necessariamente. Eles trazem sua própria sobrecarga — comparam a matriz dependencies a 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 useMemo e uma variável comum? R: useMemo possui “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, useMemo precisa comparar a matriz de dependências durante a renderização, de modo que “cálculos simples ficam, na verdade, mais lentos com useMemo”.

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 useCallback para 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 hook useEffect. Se nenhuma dessas condições se aplicar, você pode simplesmente escrever a função diretamente; useCallback é desnecessário.

P: Qual é a diferença entre useMemo e useCallback? R: useMemo(() => value, deps) armazena em cache o resultado (valor) de um cálculo, enquanto useCallback(() => fn, deps) armazena em cache uma referência a uma função. Na verdade, useCallback(fn, deps) é equivalente a useMemo(() => fn, deps). Use useCallback quando precisar armazenar em cache uma função passada para um componente filho; use useMemo quando precisar armazenar em cache o resultado de um cálculo.


📖 Resumo


📝 Exercícios

  1. Problema Básico (Dificuldade ⭐): Crie um componente PrimeCalculator que receba um número n como entrada e calcule o enésimo número primo. Use useMemo para armazenar o resultado em cache, de modo que ele não seja recalculado toda vez que um novo valor for inserido.
  2. Exercício avançado (Dificuldade ⭐⭐): Crie um componente ProductTable que contenha 500 registros de produtos e permita a filtragem por categoria e a ordenação por preço. Use useMemo para otimizar a lógica de filtragem e ordenação, e use useCallback em combinação com React.memo para otimizar a renderização da lista.
  3. Desafio (Dificuldade: ⭐⭐⭐): Crie um componente DataDashboard que agregue 10.000 registros de pedidos em três dimensões diferentes (tempo, região e categoria) e use useMemo para implementar o armazenamento em cache hierárquico (de modo que apenas as camadas relevantes sejam recalculadas quando as dimensões mudarem).
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%