Performance tuning em ReactJS
Você está trabalhando em um projeto ReactJS, a codebase parece
okay e tudo segue normalmente. Mas por algum motivo, a aplicação
apresenta lentidão, e os plugins de otimização de bundle não estão
sendo o suficiente. O quê você faz?
Olá! Eu sou Gabriel Carvalho
Desenvolvedor FrontEnd desde 2019.
Apaixonado por tecnologia e o que faço.
Por que otimizar?
Vamos começar com um questionamento. Afinal, o app já está em produção. Meu trabalho acabou, agora é só ficar na sustentação certo?
Bom, espero que você não precise chegar ao ponto de mandatoriamente ter que rever a performance do seu app. Mas se for o seu caso, ou você só está interessado em melhorar um pouco a experiencia do seu usuário, este artigo pode te ajudar.
Renderização
Você sabe como o React renderiza seus componentes?
Sempre que as props(aqueles argumentos maravilhosos passados pelo componente pai), ou o state do proprio componente mudam, o React força um ciclo de renderização no componente, e toda sua árvore.
Ou seja, se você tem uma hierarquia de 3 componentes, e o primeiro atualiza, o seu filho, e o decendente seguinte são submetidos ao ciclo de renderização.
Em uma aplicação gigante cujos componentes e hooks não tenham sido bem planejados, isso pode virar um fuê de ciclos de atualização e dados transitando para lá e para cá rapidinho.
Como apagamos o incendio então?
Iremos utilizar o React DevTools para identificar gargalos no processo de renderização dos componentes.
Temos um app simples
const App: FC = () => (
<PokeListProvider>
<FilterProvider>
<main className="App">
<header className="App-header">
<h1>
POCKEMÓNS
</h1>
</header>
<Filter />
<PokeList />
</main>
</FilterProvider>
</PokeListProvider>
);
export default App;
const PokeList: FC = () => {
const { loadingRowList, pokemons } = usePokeList();
const { pokemonsToShow } = useFilter();
return (
<section className='pokelist'>
{loadingRowList ? (
<p className='loading'>Carregando Pokemons</p>
) : pokemons.map((pokemon) => pokemonsToShow.includes(pokemon.name) && (
<PokeCard
key={pokemon.name}
pokemon={pokemon}
/>
))}
</section>
);
}
export default PokeList;
const Filter: FC = () => {
const { form, onSubmit } = useFilter();
return (
<form onSubmit={onSubmit} className="filter-form">
<input placeholder='Nome' {...form.register('name')} />
<input placeholder='Tipo' {...form.register('type')} />
<button type='submit'> Buscar</button>
</form>
);
}
export default Filter;
Notem que a renderização inicial carrega todos os cards
Quando busco por pokemons de grama
Percebeu?
Mesmo que o card do Bulbassauro não tenha saído da tela, ele renderizou de novo… E TODOS OS POKÉMONS DE GRAMA
O uso do React.memo
O “memo” é um Higher Order Component que diz ao React que, aquele componente que foi passado para ele, deve ser memorizado.
Assim o React faz um pequeno esforço incial extra para memorizar o componente, evitando que no futuro aquele item seja recriado desnecessariamente.
export default memo(PokeCard);
Note que agora não houve nenhum renderização nova
Quando retiro o filtro, agora só surgem os cards que não estavam
20,5 ms
Antes do memo
2,9 ms
Após o memo
É só usar memo em tudo
Certo?
Errado
Devemos ter muito cuidado com o uso do memo, pois ele exige mais da máquina para fazer essa memorização.
Apesar de isso ser muito util quando nosso componente é recriado sem alteração nas props.
Ele não é efetivo em componentes que atualizam muito, como PokeList. Um memo ali só tornaria nosso app um pouco mais pesado sem necessidade.
E quando temos milhares de componente em nossa aplicação, precisamos analizar com cuidado antes de sair memorizando tudo.
Tá mas e o useMemo?
Podemos também utilizar o hook “useMemo” para fazer a memorização de cálculos pesados e até mesmo listas de componentes.
Tomando o mesmo cuidado, pois useMemo também exige um pouco mais para fazer essa memorização.
Caso nosso <PokeList />
possuísse outros elementos que desencadeassem renderização, sem necessariamente afetar nossos pokemons, poderíamos usar useMemo para memorizar nossos elementos, “driblando” a renderização de <PokeList />
devido a outros elementos(props, hooks, etc).
const PokeList: FC = () => {
const { loadingRowList, pokemons } = usePokeList();
const { pokemonsToShow } = useFilter();
const cardList = useMemo(() => pokemons
.map((pokemon) => pokemonsToShow.includes(pokemon.name) && (
<PokeCard
key={pokemon.name}
pokemon={pokemon}
/>
)), [pokemons, pokemonsToShow]);
return (
<section className='pokelist'>
{loadingRowList ? (
<p className='loading'>Carregando Pokemons</p>
) : cardList}
</section>
);
}
Memo personalizado
É possível também passar uma função que valida as props e retorne um booleano para que o react recrie ou não o componente
export default memo(PokeCard, (
prevProps: Readonly<PokeCardProps>,
nextProps: Readonly<PokeCardProps>,
) => {
return JSON.stringify(prevProps.pokemon) !== JSON.stringify(nextProps.pokemon)
});
Infinite scroll
Outra técnica que podemos utilizar também é o “infinite scroll”.
Consiste basicamente em limitar a quantidade de itens exibidos em tela, e carregar mais elementos apenas quando necessário.
Ou seja, o usuário chega perto do final da página, e os itens adicionais são carregados.
Exemplo de implementação
const [scrollTimeout, setScrollTimeout] = useState(null as unknown as NodeJS.Timeout);
const onScroll = () => {
const scrolledToBottom =
window?.innerHeight + Math.ceil(window?.pageYOffset) + 300 >=
document?.body.offsetHeight;
const haveMoreToShow = amountToShow < (pokemonsToShow?.length);
if (scrolledToBottom && haveMoreToShow) {
if (scrollTimeout) clearTimeout(scrollTimeout);
setScrollTimeout(
setTimeout(() => {
setAmountToShow(amountToShow + 10);
}, 50),
);
}
};
useEffect(() => {
window?.addEventListener('scroll', onScroll);
return () => window?.removeEventListener('scroll', onScroll);
}, [onScroll]);
const { loadingRowList, pokemons } = usePokeList();
const { pokemonsToShow, amountToShow } = useFilter();
const cardList = useMemo(() => pokemons
.slice(0, amountToShow)
.map((pokemon) => pokemonsToShow.includes(pokemon.name) && (
<PokeCard
key={pokemon.name}
pokemon={pokemon}
/>
)), [pokemons, pokemonsToShow, amountToShow]);
Bonus
Falamos brevemente sobre memorização aqui. Mas existem outros tópicos que podem ser interessantes.
Experimente também
- React.lazy e code splitting
- WebWorkers
- Bibliotecas gerenciadoras de data flow (Redux, MobX, BLoC)
Obrigado!
Alguma duvida?
Você pode me encontrar em:
Creditos
Agradecimentos especiais a Mariana, por me aturar falando de código por horas e ser minha revisora