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.