Performance no React: memo, useMemo, useCallback

1. Por que a performance importa no React?

O React é conhecido por sua eficiência em atualizar a interface do usuário, mas essa eficiência não é automática. Toda vez que o estado de um componente muda, o React re-renderiza o componente e todos os seus filhos. Esse processo envolve reconciliação (reconciliation) e diffing — algoritmos que comparam a árvore virtual do DOM anterior com a nova para aplicar apenas as diferenças necessárias.

O problema surge quando componentes que não precisam ser atualizados acabam sendo re-renderizados. Em uma aplicação pequena, isso pode passar despercebido. Mas em sistemas complexos — com listas extensas, gráficos ou formulários com muitos campos — renderizações desnecessárias podem causar lentidão perceptível, travamentos na interface e consumo excessivo de memória.

É aqui que entram três ferramentas essenciais do ecossistema React: React.memo, useMemo e useCallback.

2. React.memo: evitando re-renderizações de componentes

React.memo é um higher-order component (HOC) que envolve um componente funcional e impede sua re-renderização se as props não mudarem. Ele faz uma comparação superficial (shallow comparison) das props — verifica se as referências ou valores primitivos são os mesmos da renderização anterior.

import React from 'react';

const ListItem = React.memo(({ item, onRemove }) => {
  console.log(`Renderizando item: ${item.id}`);
  return (
    <li>
      {item.name}
      <button onClick={() => onRemove(item.id)}>Remover</button>
    </li>
  );
});

export default ListItem;

Neste exemplo, ListItem só será re-renderizado se item ou onRemove mudarem. Se o componente pai re-renderizar sem alterar essas props, o ListItem permanece inalterado.

Quando funciona e quando falha: A comparação superficial funciona bem para props primitivas (string, number, boolean). Para objetos, arrays ou funções, a comparação é por referência — se o pai criar um novo objeto em toda renderização, o React.memo não impedirá a re-renderização. É aí que useCallback e useMemo entram em cena.

3. useMemo: memorizando valores computados

Enquanto React.memo memoriza componentes, useMemo memoriza valores. Ele recebe uma função de cálculo e um array de dependências. O resultado só é recalculado quando as dependências mudam.

import React, { useMemo } from 'react';

const UserList = ({ users, searchTerm }) => {
  const filteredUsers = useMemo(() => {
    console.log('Filtrando usuários...');
    return users.filter(user =>
      user.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [users, searchTerm]);

  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

Sem useMemo, o filtro seria executado em toda renderização, mesmo que users e searchTerm não tivessem mudado. Com useMemo, o cálculo só ocorre quando pelo menos uma das dependências muda.

Cuidados importantes: Memorização tem custo — o React precisa armazenar e comparar o valor anterior. Use useMemo apenas para cálculos realmente pesados (processamento de arrays grandes, transformações complexas de dados, operações matemáticas intensivas). Para cálculos simples, o custo da memorização pode superar o benefício.

4. useCallback: memorizando funções para evitar novas referências

Funções inline são um dos principais causadores de re-renderizações indesejadas. Toda vez que um componente pai re-renderiza, funções definidas dentro dele são recriadas — mesmo que o código seja idêntico ao anterior. Isso quebra a memorização de React.memo nos componentes filhos.

import React, { useState, useCallback } from 'react';
import ListItem from './ListItem';

const ListContainer = () => {
  const [items, setItems] = useState([
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' }
  ]);
  const [count, setCount] = useState(0);

  // Sem useCallback: nova referência em toda renderização
  const handleRemove = useCallback((id) => {
    setItems(prev => prev.filter(item => item.id !== id));
  }, []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Contador: {count}
      </button>
      <ul>
        {items.map(item => (
          <ListItem key={item.id} item={item} onRemove={handleRemove} />
        ))}
      </ul>
    </div>
  );
};

Aqui, handleRemove mantém a mesma referência entre renderizações graças ao useCallback. Combinado com React.memo no ListItem, o clique no contador não força a re-renderização dos itens da lista.

Sinergia com React.memo: useCallback é mais útil quando usado em conjunto com React.memo. Se o componente filho não estiver memorizado, a função estável não trará benefício de performance — mas ainda pode ser útil para efeitos colaterais ou hooks como useEffect.

5. Mitos, armadilhas e boas práticas comuns

Mito: "Usar memo/useMemo/useCallback sempre melhora a performance" — Falso. Cada uma dessas ferramentas tem custo computacional. React.memo precisa comparar props, useMemo armazena valores em cache, useCallback mantém referências. Em componentes simples ou cálculos baratos, o custo pode superar o benefício.

Armadilha: Dependências vazias ou incorretas — Dependências incorretas causam bugs silenciosos. Use o ESLint com o plugin eslint-plugin-react-hooks para garantir dependências corretas.

Quando NÃO usar:
- Componentes que sempre recebem props diferentes (ex: listas com keys únicas)
- Cálculos leves (operações matemáticas simples, formatação de strings)
- Funções passadas para elementos nativos do DOM (como onClick em <button>)
- Componentes que re-renderizam com pouca frequência

6. Ferramentas de diagnóstico e profiling

Antes de otimizar, meça. O React DevTools Profiler é a ferramenta mais direta para identificar re-renderizações desnecessárias:

// Exemplo de uso com performance.now()
const startTime = performance.now();
// código a ser medido
const endTime = performance.now();
console.log(`Tempo de execução: ${endTime - startTime}ms`);

Bibliotecas como why-did-you-render podem ser injetadas durante o desenvolvimento para alertar sobre re-renderizações suspeitas:

import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, {
  trackAllPureComponents: true,
});

7. Estratégias avançadas de otimização

Combinação de useMemo com useCallback em contextos complexos: Quando um contexto fornece objetos ou arrays, use useMemo para estabilizá-los e useCallback para funções:

const AppProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const contextValue = useMemo(() => ({
    state,
    dispatch
  }), [state, dispatch]);

  return (
    <AppContext.Provider value={contextValue}>
      {children}
    </AppContext.Provider>
  );
};

useMemo para estabilizar objetos passados como props: Se um componente filho recebe um objeto configurado dentro do pai, encapsule-o em useMemo:

const config = useMemo(() => ({
  theme: 'dark',
  language: 'pt-BR'
}), []);

Complemento com lazy loading e code splitting: Memorização não substitui estratégias como React.lazy e Suspense para carregamento sob demanda de componentes pesados.

Lembre-se: otimização prematura é a raiz de todo mal. Meça primeiro, identifique gargalos reais e só então aplique React.memo, useMemo e useCallback nos pontos críticos. A performance de uma aplicação React não vem de aplicar essas ferramentas em todo lugar, mas de usá-las com inteligência cirúrgica onde realmente fazem diferença.

Referências