FNSP Web Page

useMemo e useCallback: quando compensam, e quando são só ruído

Há duas formas de usar mal estes dois hooks, e são opostas. Uma é nunca lhes tocar e ter uma aplicação a arrastar-se. A outra — muito mais comum — é embrulhar tudo neles por precaução, e ficar com um código mais difícil de ler que continua exatamente igual de rápido.

1. O que fazem, em duas linhas

// Guarda um valor entre renders
const visiveis = useMemo(() => filtrar(artigos, etiqueta), [artigos, etiqueta]);

// Guarda uma função entre renders
const aoGuardar = useCallback((dados) => gravar(id, dados), [id]);

Enquanto as dependências não mudarem, o React devolve o que já tinha. E há uma letra pequena que convém conhecer: isto é uma optimização, não uma garantia. O React pode deitar a cache fora quando lhe der jeito. Nunca ponha lá dentro nada de que a correção do programa dependa.

2. O useCallback não é sobre velocidade

Este é o mal-entendido mais caro. Criar uma função em JavaScript custa quase nada — não é isso que se está a poupar.

function Pai() {
  const [texto, setTexto] = useState('');

  // Isto é barato. O problema não é criá-la.
  const aoClicar = () => abrir(id);

  return <Filho aoClicar={aoClicar} />;
}

O que se está a poupar é a identidade. Aquela função é um objeto diferente em cada render, e tudo o que compare props por identidade — ou dependências de um efeito — conclui, com razão, que mudou.

3. A parte que quase toda a gente esquece

Um useCallback numa prop não faz absolutamente nada se o filho não estiver embrulhado em memo.

// Trabalho a mais, efeito nenhum: o Filho desenha na mesma
const aoClicar = useCallback(() => abrir(id), [id]);
return <Filho aoClicar={aoClicar} />;

// Só com isto é que a estabilidade da prop passa a valer alguma coisa
const Filho = memo(function Filho({ aoClicar }) {
  ...
});

Sem o memo, o filho volta a desenhar porque o pai desenhou, e a identidade da prop é irrelevante. Os dois andam sempre aos pares: memoizar a prop sem memoizar o filho é meio gesto, e meio gesto aqui é gesto nenhum.

4. Os três casos em que compensam mesmo

  • Um cálculo realmente caro — e caro quer dizer medido no Profiler, não pressentido. Filtrar mil objetos não é caro; ordenar cem mil ou correr uma expressão regular complicada sobre um texto grande pode ser.

  • Uma prop que vai para um filho em memo, pelo motivo da secção anterior.

  • Um valor que entra numa lista de dependências — de um efeito, ou de outro useMemo.

Fora destes três, o mais provável é estar a pagar sem receber.

5. Muitas vezes o problema foi criado duas linhas acima

// "opcoes" é um objeto novo a cada render: o efeito corre sempre
const opcoes = { salaId, tema };

useEffect(() => {
  ligar(opcoes);
}, [opcoes]);

// Podia memoizar-se o objeto. Ou não se criar objeto nenhum.
useEffect(() => {
  ligar({ salaId, tema });
}, [salaId, tema]);

A segunda versão não precisa de useMemo. É o mesmo raciocínio dos efeitos que não eram precisos: antes de guardar o resultado, vale a pena perguntar se o problema não desaparece mudando a forma como o valor nasce.

6. O erro clássico: a lista de dependências

// A lista vazia parece inofensiva e congela o texto do primeiro render.
// Meses depois, o formulário publica sempre a mesma coisa.
const enviar = useCallback(() => {
  publicar(texto);
}, []);

// Com a dependência certa
const enviar = useCallback(() => {
  publicar(texto);
}, [texto]);

É o custo escondido destes hooks: cada um acrescenta uma lista que tem de ser mantida à mão e que ninguém revê. Um componente sem memoização nenhuma não tem como ficar com um valor velho preso lá dentro.

7. Antes de memoizar, mexa na estrutura

Grande parte dos renders a mais resolve-se sem hook nenhum: descendo o estado para onde ele é usado, e deixando passar por children o que não depende dele.

// Cada movimento do rato volta a desenhar a <Pesada />
function App() {
  const [x, setX] = useState(0);

  return (
    <div onMouseMove={(e) => setX(e.clientX)}>
      <Pesada />
    </div>
  );
}

// O estado desce, e a <Pesada /> chega como children:
// deixa de ser recriada quando o x muda
function App() {
  return (
    <Moldura>
      <Pesada />
    </Moldura>
  );
}

function Moldura({ children }) {
  const [x, setX] = useState(0);
  return <div onMouseMove={(e) => setX(e.clientX)}>{children}</div>;
}

Na segunda versão o elemento <Pesada /> é criado pela App, que não desenha quando o x muda. O React vê a mesma referência e salta-o. Sem memo, sem useMemo, sem lista de dependências.

8. E agora existe um compilador

O React Compiler faz esta memoização sozinho, ao construir: analisa o componente e insere as caches onde elas fazem sentido, sem listas de dependências para alguém enganar. Onde estiver ligado, escrever useMemo e useCallback à mão passa a ser, na maior parte dos casos, trabalho repetido.

Vale a pena confirmar se está ligado no seu projeto antes de decidir seja o que for — a resposta muda completamente a conclusão deste artigo.

O resumo

Meça primeiro, no Profiler. Se não houve medição, não há problema de desempenho: há um palpite. E um palpite não paga o preço destes dois hooks, que se cobra em listas de dependências, em código mais difícil de ler, e em valores velhos presos onde ninguém os vai procurar.

Comentários

Ainda ninguém comentou este artigo.

Voltar ao blog

Gostávamos de saber quantas pessoas visitam o site, com o Google Analytics. Sem a sua autorização não corre nada. Política de Privacidade.