Profile image

Performance com React.memo & useMemo & useCallback

Primeiramente, a gente tem que entender como que funciona o processo de re-renderização do React, e aí sim entender quando e como que a memorização pode ser útil.

Basicamente, podemos dizer que toda re-renderização do React.js é causada por conta de um estado que foi alterado.

Mas isso não significa que apenas o estado do componente é responsável por uma re-renderização. Calma! Vamos entender isso com calma.

A mudança de estado é o inicio de tudo, mas muita coisa acontece por conta disso e a gente tem que entender como que o React.js funciona por baixo dos panos.

Vamos focar em dois principais gatilhos que fazem um componente ser re-renderizado;

Quando uma propriedade do componente muda;

Se um estado que ta sendo passado por propriedade pra um componente é alterado, logo, esse componente precisa ser re-renderizado também, por conta disso sempre que uma propriedade de um componente muda, ele é re-renderizado.

Quando o componente pai é re-renderizado;

Quando um componente é re-renderizado, tudo nele é “recriado“ novamente, e isso inclui tudo que está no return também, então se um componente pai é re-renderizado, todos os seus filhos também são re-renderizados.

Na prática

É um pouco confuso no começo, mas na prática é um pouco mais fácil de entender o que está acontecendo por trás dos panos.

Então vamos iniciar pensando em um componente assim:

export const App = () => {
const [switcherValue, setSwitcherValue] = useState(false);
const handleToggle = () => setSwitcherValue((prev) => !prev);
return (
<div>
<Title>Without Memo</Title>
<SwitcherContainer onToggle={handleToggle} enabled={switcherValue} />
</div>
);
}

Without Memo

Rendered: 1 times
Rendered: 1 times
Rendered: 1 times

Nesse exemplo a gente consegue perceber o básico do react funcionando.

O componente App está sendo renderizado toda vez que o switcherValue é alterado.

O componente SwitcherContainer está sendo renderizado toda vez que o estado switcherValue é alterado, por que o switcherValue está sendo passado como propriedade pra ele, logo ele realmente precisa ser re-renderizado!

E a gente consegue perceber também, que o Title também foi re-renderizado, apenas por ser um componente filho de um componente que está sendo re-renderizado.

Primeiro caso de memorização;

Nesse momento já conseguimos pensar no nosso primeiro caso de memorização.
Afinal, não tem a menor necessidade do componente Title renderizar junto, sendo que ele é basicamente um “componente estático“, nenhuma propriedade dele foi alterada.

É aí que entra a primeira mágica, o React.memo, que basicamente faz com que um componente só seja re-renderizado se alguma propriedade que ele recebe for alterada. Mesmo que o componente pai seja re-renderizado.

Logo, isso resolveria nosso problema:

export const Title = React.memo(({ children }: TitleProps) => {
return (
<h1>{children}</h1>
);
});
Title.displayName = "Title";

E o componente App continuaria igual

export const App = () => {
const [switcherValue, setSwitcherValue] = useState(false);
const handleToggle = () => setSwitcherValue((prev) => !prev);
return (
<div>
<Title>With Memo</Title>
<SwitcherContainer onToggle={handleToggle} enabled={switcherValue} />
</div>
);
}

With Memo

Rendered: 1 times
Rendered: 1 times
Rendered: 1 times

Agora, o Title não é re-renderizado mais, por que nenhuma propriedade nem estado dele estão sendo alterados.

O SwitcherContainer continua re-renderizando por que ele realmente depende do estado, não é um re-render desnecessário.

Fácil demais;

Até então ta muito fácil, na vida real nem sempre é assim tão simples, vamos complicar um pouco mais agora. E se nesse componente, a gente conseguisse alterar o estado a partir de dois lugares diferentes...

export const App = () => {
const [switcherValue, setSwitcherValue] = useState(false);
const [anotherSwitcherValue, setAnotherSwitcherValue] = useState(false);
const handleToggleSwitcherValue = () => setSwitcherValue((prev) => !prev);
const handleToggleAnotherSwitcherValue = () => setAnotherSwitcherValue((prev) => !prev);
return (
<div>
<Title>Without Memo</Title>
<SwitcherContainer onToggle={handleToggleSwitcherValue} enabled={switcherValue} />
<SwitcherContainer onToggle={handleToggleAnotherSwitcherValue} enabled={anotherSwitcherValue} />
</div>
);
}

Without Memo

Rendered: 1 times
Rendered: 1 times
Rendered: 1 times
Rendered: 1 times

Bom... O mesmo problema que tivemos com o Title certo? Um SwitcherContainer está sendo re-renderizado quando eu altero o valor do outro. Só adicionar um React.memo que vai ser resolvido.

E se eu te falasse que o SwitcherContainer já está usando o React.memo?

Então o que está acontecendo? Nesse caso, o problema é um pouco diferente...

Note que uma das propriedades que está sendo passada pro SwitcherContainer é uma função (onToggle).

Quando comparamos duas funções javascript, elas são consideradas iguais apenas se elas tiverem a mesma referência.
Ou seja, mesmo se o conteúdo das funções forem exatamente IGUAL, se ela tiver sido “recriada“, o javascript vai considerar que são funções diferentes.

Exemplo:

const function1 = () => console.log("Hello World");
const function2 = () => console.log("Hello World");
console.log(function1 === function2); // false
const function3 = function1;
console.log(function1 === function3); // true

Então o que está acontecendo é:

Como resolver isso? Aí que entra o segundo feitiço, o useCallback

O useCallback basicamente memoriza a função e faz com que ela não seja re-criada toda vez que o componente é re-renderizado, preservando a referência da função e assim evitando re-render desnecessário por conta do React achar que alguma propriedade mudou.

É como se em cada re-render, o React invés de “recriar“ a função, ele reutilizasse a mesma que ele guardou na memória (caso nenhuma dependencia tenha sido alterada)

export const App = () => {
const [switcherValue, setSwitcherValue] = useState(false);
const [anotherSwitcherValue, setAnotherSwitcherValue] = useState(false);
const handleToggleSwitcherValue = useCallback(() => setSwitcherValue((prev) => !prev), []);
const handleToggleAnotherSwitcherValue = useCallback(() => setAnotherSwitcherValue((prev) => !prev), []);
return (
<div>
<Title>Without Memo</Title>
<SwitcherContainer onToggle={handleToggleSwitcherValue} enabled={switcherValue} />
<SwitcherContainer onToggle={handleToggleAnotherSwitcherValue} enabled={anotherSwitcherValue} />
</div>
);
};

With Memo

Rendered: 1 times
Rendered: 1 times
Rendered: 1 times
Rendered: 1 times

Agora sim! Agora o React consegue identificar exatamente quando a função é alterada de fato e a gente consegue evitar o re-render desnecessário!

OBS: É obrigatório o uso do React.memo, caso contrário, nessa situação, o useCallback não terá efeito nenhum, sem o React.memo cairá na mesma situação do primeiro cenário, o SwitcherContainer vai re-renderizar apenas por que o componente pai re-renderizou...

Complicando um pouco mais...

Ok, agora que entedemos como o useCallback funciona, vamos complicar um pouco mais o nosso componente...

Na vida real, trabalhamos com dados vindo de diversos lugares e é muito comum utilizar estruturas como Array e Objetos pra transportar esses dados

E se o nosso componente passasse um objeto ou um array como propriedade pra algum filho?

export const App = () => {
const [switcherValue, setSwitcherValue] = useState(false);
const [anotherSwitcherValue, setAnotherSwitcherValue] = useState(false);
const handleToggleSwitcherValue = useCallback(() => setSwitcherValue((prev) => !prev), []);
const handleToggleAnotherSwitcherValue = useCallback(() => setAnotherSwitcherValue((prev) => !prev), []);
const user = {
name: "John",
age: 25,
}
return (
<div>
<Title>Without Memo</Title>
<User user={user} />
<SwitcherContainer onToggle={handleToggleSwitcherValue} enabled={switcherValue} />
<SwitcherContainer onToggle={handleToggleAnotherSwitcherValue} enabled={anotherSwitcherValue} />
</div>
);
};

Without Memo

Rendered: 1 times

Name: John

Age: 25

Rendered: 1 times
Rendered: 1 times
Rendered: 1 times
Rendered: 1 times

Bom, o mesmo problema que tivemos com as funções, acontece com os objetos/arrays também...
Com o javascript é a mesma coisa, se você criar um objeto novo, ele vai ter uma referência diferente do objeto anterior, mesmo que o conteúdo seja o mesmo.

Exemplo:

const object1 = { name: "John", age: 25 };
const object2 = { name: "John", age: 25 };
console.log(object1 === object2); // false
const object3 = object1;
console.log(object1 === object3); // true

Então o que está acontecendo é:

Mas dessa vez não podemos usar o useCallback pra resolver isso, por sorte temos um cara tão bom quanto chamado useMemo que faz exatamente a mesma coisa, mas para dados estáticos como objetos e arrays. (Ou qualquer coisa que não seja uma função)

export const App = () => {
const [switcherValue, setSwitcherValue] = useState(false);
const [anotherSwitcherValue, setAnotherSwitcherValue] = useState(false);
const handleToggleSwitcherValue = useCallback(() => setSwitcherValue((prev) => !prev), []);
const handleToggleAnotherSwitcherValue = useCallback(() => setAnotherSwitcherValue((prev) => !prev), []);
const user = useMemo(() => ({
name: "John",
age: 25,
}), [])
return (
<div>
<Title>With Memo</Title>
<User user={user} />
<SwitcherContainer onToggle={handleToggleSwitcherValue} enabled={switcherValue} />
<SwitcherContainer onToggle={handleToggleAnotherSwitcherValue} enabled={anotherSwitcherValue} />
</div>
);
};

With Memo

Rendered: 1 times

Name: John

Age: 25

Rendered: 1 times
Rendered: 1 times
Rendered: 1 times
Rendered: 1 times

Agora sim! O User não é re-renderizado mais

Agora o que está acontecendo é:

OBS: Mesma coisa do useCallback. É obrigatório o uso do React.memo, caso contrário, nessa situação, não terá efeito nenhum, sem o React.memo cairá na mesma situação do primeiro cenário lá em cima, o User vai acabar sendo re-renderizado apenas por que o componente pai re-renderizou...

Mas não é só pra isso que serve o useMemo e o useCallback

Terão vezes que a gente vai precisar usar o useMemo e o useCallback pra resolver outras coisas que não sejam apenas evitar re-renderizações desnecessárias.

Os exemplos que eu dei eram todos focados em evitar re-renderizações em conjunto com o React.memo, mas eles tem outras utilidades também.

Por exemplo, o useMemo pode ser usado pra fazer cálculos pesados e evitar que eles sejam refeitos toda vez que o componente é re-renderizado.

const sum = useMemo(() => {
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
return result;
}, [])

Imagina toda hora que um componente re-renderizar (agora que você sabe como funciona) ele ter que re-fazer esse cálculo de novo... Não é muito legal né?

O useCallback e useMemo também pode ser usado pra evitar disparar um useEffect desnecessariamente, por exemplo.

const handleFunction = useCallback(() => {
console.log("Hello World");
}, []);
useEffect(() => {
handleFunction();
}, [handleFunction]);

Sem o useCallback nesse caso, toda vez que o componente re-renderizar, ohandleFunction seria recriado, e o useEffect seria disparado, por que ele não conseguiria identificar que a função é a mesma. Por conta que a referência da função mudou.

E o mesmo vale pro useMemo, se você tiver colocando objetos ou arrays como dependências de um useEffect, por exemplo, e toda vez que o componente re-renderizar, ele vai recriar o objeto e o useEffect vai ser disparado, por que ele não vai conseguir identificar que o objeto é o mesmo.

Mas isso foi apenas pra mostrar que o useCallback e o useMemo não server apenas pra serem utilizado em conjunto com o React.memo, eles tem outras utilidades também.

Considerações finais

Claro que os exemplos que eu dei aqui são bem simples e não refletem necessariamente a realidade de todas as aplicações, mas a ideia é mostrar como o React.memo, useMemo e o useCallback podem ser úteis em situações do dia a dia.

Muitas vezes acabam usando o useMemo e o useCallback sem realmente entender o que está acontecendo, e a longo prazo isso pode acabar se tornando um problema. Por mais que o custo seja pequeno, o useMemo e o useCallback tem um custo de performance, afinal, eles estão fazendo um trabalho extra por trás dos panos.

Então é sempre bom entender como eles funcionam de verdade pra saber quando realmente é necessário usá-los.

Mas também não adianta fazer um terror em cima disso...

Na minha humilde opinião, na maioria das vezes, em aplicações pequenas, o uso deuseMemo e useCallback em excesso não vai causar um impacto tão grande na performance... Teria que ser um uso muito grande pra isso começar realmente a ficar perceptível.

É mais fácil um problema de performance ser causado por conta da ausencia deles do que pelo excesso deles. Mas use com moderação! 😂

Fique a vontade pra instalar o repositório e fazer mais testes por conta própria!

Um detalhe importante que vale ser mencionado é que o React está trabalhando em um compilador muito mais robusto, que será capaz de fazer muitas dessas memorizações automáticamente sem precisar ficar passando React.memo, etc... O novo compilador até então está em beta, mas já da pra ser testado por quem quiser.
Mais informações: React Compiler