Executando verificação de segurança...
5

Performance tuning em ReactJS

English version

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.

Github LinkedIn Instagram

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:

Github LinkedIn Instagram

Creditos

Agradecimentos especiais a Mariana, por me aturar falando de código por horas e ser minha revisora

Apresentação Powerpoint

Link para o projeto

Carregando publicação patrocinada...
2

Webworkers e React.Lazy hoje são indispensáveis, à exemplo o próprio Tabnews que poderia usar mais dynamic no site, o bom é que eles não carregam muitos scripts de terceiros, logo não percebemos tanto o gargalo FCP.

Ótima publicação.

1

Obrigado pela vibe, esqueci de comentar no artigo, mas o projeto linkado está rodando a lógica de filtros em WebWorkers, vale a pena conferir o projeto no Github.