Neste texto, vamos falar sobre como tornar aplicações React mais rápidas e eficientes. Voltado para pessoas desenvolvedoras e interessadas em tecnologia, o conteúdo vai mostrar técnicas importantes para melhorar o desempenho. A relevância disso está em criar experiências de pessoa usuária mais ágeis e escaláveis.

Antes de começarmos a entender algumas técnicas para otimização de aplicações React, é muito importante entender como o React faz uma renderização e a árvore de componente na prática. Passarei por esse tópico rapidamente, já que não é o foco do artigo, porém, você pode encontrar mais informações na própria documentação do React.

React Conciliation e DOM

Quando falamos da alta performance de renderização de aplicações desenvolvidas utilizando React, um dos motivos que mais são importantes quando se toca neste ponto é o React Conciliation.

O React nunca atualiza o DOM original diretamente. Para cada objeto DOM, será criada uma cópia na memória a ele correspondente. Essa cópia é chamada de Virtual DOM.

Na árvore Virtual DOM, cada elemento é representado por um nó. Uma nova árvore Virtual DOM será criada sempre que o estado de um elemento mudar. Havendo uma mudança, o algoritmo de diferenciação do React irá comparar a árvore do Virtual DOM atual com sua versão anterior.

Finalmente, o Virtual DOM usa o algoritmo para atualizar o DOM real com a diferença entre eles.

Demonstração de como ocorre o React Conciliation. Fonte:

Esse mecanismo para diferenciar uma árvore com outra para determinar quais partes precisam ser alteradas e então atualizar o DOM original com ela é chamado Reconciliation.

Estratégias e Otimizações

Apesar do React já ter essa inteligência e conseguir entender o que foi alterado para fazer uma nova renderização, podemos deixar isso ainda mais otimizado de forma a conseguir controlar melhor quais elementos e componentes devem ser atualizados, em que momento isso acontece entre outras possibilidades.

Veremos algumas formas.

Memo

Em linhas gerais e de forma direta, utilizar o Memo fará com que o React não renderize um componente se suas props não tiverem sofrido alguma alteração. Dessa forma, colocamos o Memo em volta da exportação do componente:

Exemplo do uso de Memo

No exemplo acima, o componente Child só será re-renderizado se os valores passados como props para ele mudar, caso contrário, apenas os componentes acima serão re-renderizados.

useMemo

O useMemo é um hook do próprio React retorna um valor memorizado. Pense na memoização como o armazenamento em cache de um valor para que ele não precise ser refeito caso não tenha sido alterado.

Dessa forma, o useMemo só irá retornar um novo valor caso ele tenha sido alterado:

Exemplo de código utilizando useMemo

No exemplo acima, o useMemo está sendo usado no retorno do valor da função calculatedFactorial e utilizando a variável num como parâmetro dentro dos colchetes []; nos colchetes sempre é passado aquilo que quero “observar” caso mude. Sendo assim, sempre que a variável nume tiver seu valor alterado o useMemo entende que o código dentro dele deverá ser executado novamente, caso contrário é utilizado o valor previamente memoizado.

useCallback

Semelhante ao useMemo, o useCallback também é um hook do React que trabalha com a memoização, mas neste caso, de funções.

A cada renderização do seu componente, todo o código que está nele é executado novamente. Portanto, as funções são re-declaradas, e uma nova referência (na memória), é alocada para cada função. O useCallback faz com que sua função seja redefinida apenas quando necessário, assim mantendo a mesma referência:

Exemplo de código usando useCallback

No exemplo acima, o useCallback guarda a referência da função add (linha 3) e só chama ela novamente caso os parâmetros passados dentro dos colchetes tenham seus valores alterados.

Code Splitting

Dynamic import

Uma das melhores maneiras de manter um bom desempenho para aplicativos React modernos é adiar o carregamento de algumas partes de uma página da Web até que uma pessoa usuária precise delas. Isso pode ser feito adiando a importação de componentes na aplicação caso não seja necessário imediatamente que uma página carregue.

ANTES:

Exemplo da doc do React:

No bloco de código acima, importamos os arquivos usando importação estática, e isso agrupará todos os arquivos quando o webpack for executado no código acima.

DEPOIS:

Exemplo da doc do React:

Ao contrário dos componentes importados estaticamente, as importações dinâmicas são semelhantes ao node.js, o que significa que são assíncronas e só importam seus componentes e arquivos quando necessário.

Embora as importações dinâmicas sejam uma ótima maneira de melhorar o desempenho de seus aplicativos React, existem casos em que devem ser aplicadas:

  • As importações dinâmicas podem ser usadas quando há necessidade de modulação de código e busca de dados de um servidor;
  • As importações dinâmicas podem ser usadas quando os componentes não são necessários quando um aplicativo ainda está sendo carregado;
  • As importações condicionais.

React.lazy()

A função React.lazy() permite renderizar uma importação dinâmica de um componente normalmente. Basicamente, React.lazy() faz uma chamada para uma importação dinâmica e retorna uma Promise.

ANTES:

Exemplo da página “Blog” sendo carregada de forma estática

DEPOIS:

Exemplo da página “Blog” sendo carregada utilizando a técnica de lazy loading

A página “Blog” agora é carregada lentamente, garantindo que o bloco seja carregado apenas quando for de fato renderizado.

React.Suspense()

Comumente utilizado com o React.lazy(), os permite “suspender”, deixando em stand-by, condicionalmente a renderização de um componente até que ele seja carregado em nível de visualização de tela do usuário.

A prop fallback que é passada aceita qualquer elemento React que você queira renderizar enquanto espera o carregamento do componente. Você pode colocar o Suspense em qualquer lugar acima do componente que utiliza o lazy loading. Você pode, até mesmo, envolver vários componentes com lazy loading com um único componente de Suspense.

Dica final

Bundle analyzer

Ter essa visualização abrangente do bundle (que é gerado após executarmos o comando de build), nos permite entender o tamanho final da nossa aplicação e quais dependências estão a tornando mais pesada ou não. Para isso, podemos utilizar um pacote que traz essas informações para nós, como o webpack-bundle-analyzer.

Como resultado, é possível visualizar algo semelhante à imagem abaixo, representando quanto cada pacote da sua aplicação ocupa no bundle final da sua aplicação e quanto isso está impactando.

Aqui saliento, inclusive, a importância de importar pacotes de forma correta, entendendo o que são dependências de desenvolvimento e que serão utilizadas apenas durante a fase da programação, e dependências que são globais e que devem estar no bundle final.

Conclusão

O desempenho dos sistemas sempre é uma questão muito importante ao desenvolvê-los, especialmente quando são robustos e desejam ser escaláveis.

Sem oferecer uma interface bem intuitiva e focada em usabilidade, uma aplicação pode ter desempenho abaixo do esperado, o que também acontece quando o tempo de carregamento e a segurança dos dados não é trabalhada, colocando em risco a credibilidade do sistema e retenção de clientes, por exemplo.

O principal motivo para nos preocuparmos com desempenho é dar uma experiência melhor a quem usa o sistema!

E você, o que tem aplicado em seus sistemas web para melhorar cada vez a performance deles? Deixe nos comentários suas percepções e não esqueça de compartilhar esse conteúdo com mais pessoas.

Artigo escrito por Aryanne Silva, da Comunidade PrograMaria.