Você sabe como o React renderiza seus componentes?

Otimização de performance em aplicações React

Você sabe como o React renderiza seus componentes?

TLDR;

O principal ponto de performance em aplicações React são relacionados a processos redundantes e a verificação da diferença do DOM dos componentes (estado atual > próximo estado). Para evitar esses gargalos, devemos retornar false no ciclo shouldComponentUpdate no componente mais alto possível da sua aplicação.

Para facilitar ainda mais a vida, pense no seguinte:

  • Faça uma verificação rápida em shouldComponentUpdate
  • Faça uma verificação simples em shouldComponentUpdate

Em vídeo

Você pode assistir a minha gravação falando sobre esse tópico no Youtube.


Antes de mais nada

Os exemplos nesse artigo irão utilizar React + Redux. Se você está usando outra biblioteca para data-flow, os princípios desse artigo serão úteis, porém, a implementação pode ser diferente!

Eu não usei nenhuma biblioteca de imutabilidade nesse artigo, todos os exemplos estão usando ES6 e um pouco de ES7. Alguns pontos ficam mais fáceis com imutabilidade, mas é fora do escopo desse artigo.


Qual é o maior gargalo de performance em aplicações React?

  1. Processos redundantes em componentes que não necessitam atualizar o DOM
  2. A verificação de diferença do DOM que não precisa ser atualizado — A verificação de diferença do DOM no React é incrível e facilita muito a renderização, porém, não é computacionalmente necessário realiza-lá há todo momento.

Qual é a renderização padrão do React?

Vamos olhar de perto como o React renderiza os componentes.

Renderização inicial

Na renderização inicial, nós precisamos que a aplicação inteira seja renderizada. (Nós verdes, eles serão renderizados)

Todos os nós serão renderizados, isso é ótimo! Nossa aplicação está representando o estado inicial dela.

Atualizando o estado inicial

Nós queremos atualizar um pedaço do nosso estado inicial. Essa mudança é relevante, apenas, para um nó da nossa aplicação.

Atualização ideal

Nós queremos atualizar apenas os nós relacionados com esse estado que irá ser atualizado.

O que acontece de verdade

Por padrão, isso é o que o React irá renderizar se você não disser qual nó precisa (ou não) ser atualizado. (Nós laranjas, desperdício de processo de renderização)

Opa! Todos os nossos nós estão sendo renderizados.

Todos os componentes em React chamam a função shouldComponentUpdate(nextProps, nextState). É responsabilidade dessa função retornar true, se o componente realmente precisa ser renderizado novamente, ou false, se o componente não precisa ser renderizado. Retornando false, nós eliminamos o processado de chamada da função render() desse componente.

O padrão do React é sempre retornar true na função shouldComponentUpdate, mesmo que você não defina essa função no seu componente, por baixo dos panos, o React faz isso.

Isso significa que, por padrão, toda vez que você atualizar o estado da sua aplicação, todos os componentes irão ser renderizados novamente. Esse é um grande gargalo de performance.

Como conseguir uma atualização ideal?

Tente retornar false da função shouldComponentUpdate no componente mais alto da sua hierarquia, se possível.

Para facilitar ainda mais a vida, pense no seguinte:

  • Realize uma verificação rápida em shouldComponentUpdate
  • Realize uma verificação simples em shouldComponentUpdate

Realizando uma verificação rápida em shouldComponentUpdate

Evite realizar verificação aninhadas (ou deep equality) na sua função shouldComponentUpdate, essas verificação se tornam muito caras, especialmente em larga escala ou em estrutura de dados complexas.

Uma alternativa, é mudar a referência em memória do objeto, quando seus valores mudarem.

Podemos usar essa técnica em um reducer no Redux:

Seguindo esse modelo, tudo o que você precisa fazer em sua função shouldComponentUpdate é verificar o endereço de memória do seu estado:

Um exemplo de implementação da função isObjectEqual:

Realizando uma verificação simples em shouldComponentUpdate

Primeiramente, vamos ver um exemplo de uma verificação complexa em shouldComponentUpdate:

Estruturando seu estado dessa maneira, deixa sua vida um pouco difícil ao utilizar shouldComponentUpdate:

Problema 1: Uma função enorme dentro de shouldComponentUpdate

Você pode ver o quão grande e complexo a nossa função shouldComponentUpdate ficou, e isso porque estamos utilizando dados bem simples. Isso aconteceu porquê a função precisa saber sobre a estrutura de dados e como eles se relacionam um com os outros. A complexidade e tamanho da função shouldComponentUpdate cresce conforme sua estrutura de dados. Isso facilmente nos direciona para dois erros:

  1. Sua função retorna false quando você tem atualizações (estado da aplicação sendo representado incorretamente)
  2. Sua função retorna true quando você não tem atualizações (problemas de performance)

Porque dificultar sua vida? Você quer que essas verificações sejam simples, para não precisar ficar pensando muito nelas.

Problema 2: Unindo demais componentes pais com filhos

Geralmente, aplicações devem promover flexibilidade entre componentes (componentes conhecem o mínimo possível de outros componentes). Componentes pais devem saber o mínimo possível sobre como os componentes filhos funcionam. Isso nos permite mudar o componente filho internamente sem a necessidade de mudar o componente pai (assumindo que as PropTypes continuem as mesmas). Isso também permite que os componentes filhos operem em isolamento sem a necessidade de um controle unido ao comportamento do componente pai.

Uma solução: Desnormalize sua estrutura de dados

Se você desnormalizar (ou apenas, transformar) sua estrutura de dados, podemos voltar a usar verificação de referência entre nossos objetos.

Estruturando seu objeto dessa maneira, nos permite realizar uma verificação simples em shouldComponentUpdate:

Se você quiser mudar a chave interaction, você precisa alterar a referência do objeto:

Casos especiais: Verificação de referência e props dinâmicas

Vamos analisar um exemplo de criação de props dinâmicas:

Geralmente, nós não criamos props dentro de componentes e passamos isso para os componentes filhos. Porém, é mais comum realizar isso dentro de loops:

Isso é bem comum quando criamos funções para renderizar dados:

Estratégias para resolver problemas de performance em props dinâmicas

  1. Evite criar props dinâmicas dentro de componentes

Tente melhorar o seu modelo de estrutura de dados, dessa maneira, você passará props diretamente

2. Passe props dinâmicas que possam satisfazer a verificação ===.

Exemplo:

  • boolean
  • number
  • string

Se você realmente precisar passar um objeto dinâmico, tente transformá-lo em uma representação diferente, para satisfazer ===, exemplo:

Casos especiais: Funções

  1. Não passe funções, se você puder evitar. Melhor ainda, delegue isso para o componente filho, deixe ele usar o dispatch() quando ele quiser. Uma vantagem disso é remover lógica de negócio do seu componente.
  2. Ignore a verificação de funções em shouldComponentUpdate. É impossível saber se algo dentro da função mudou.
  3. Uma dica, tente criar um objeto de funções que não mudem. Você pode colocar isso no state do seu componente no ciclo componentWillReceiveProps. Nessa maneira, suas renderizações não receberam uma nova referência. Esse método é extremamente custoso, porque você terá que manter um objeto e atualizar uma lista de funções conforme for desenvolvendo.
  4. Cria um componente intermediário para realizar os corretos bindings do this nas funções. Isso também não é ideal, porém funciona, afinal, você está introduzindo uma nova camada na sua hierarquia.
  5. Tudo o que você puder pensar para evitar a criação de novas funções nas chamadas render().

Exemplo do item 4:

Ferramentas para performance

Todas as dicas e regras acima foram encontradas usando ferramentas de detecção de performance. Usando essas ferramentas, irá te ajudar a achar os gargalos de performance específicos da sua aplicação.

console.time

Esse é bem simples:

  1. Inicie o contador
  2. Execute suas funções
  3. Pare o contador

Uma maneira bacana de usar isso é criar um middleware em Redux:

Usando esse método, você pode detectar todo o tempo necessário que cada ação levou para realizar uma renderização na sua aplicação. Você irá notar rapidamente quais ações levaram mais tempo, isso te dará um bom lugar para começar a procurar os gargalos da sua aplicação.

React.perf

Esse utiliza a mesma idéia do item acima, porém, usamos a ferramenta de performance do React:

  1. Perf.start()
  2. Execute suas funções
  3. Perf.stop()

Exemplo de um middleware:

Parecido com console.time, esse método te mostra as métricas de performance para cada ação, incluindo dos componentes React. Você pode verificar mais detalhes na documentação do React.

DevTools no navegador

Gráficos do CPU Profiler podem ser úteis em achar problemas de performance na sua aplicação.

Os gráficos de CPU mostram o estado do processo do JavaScript do seu código a cada milissegundos durante a gravação do profiler de performance. Isso te possibilita saber exatamente qual função estava sendo executada naquele determinado ponto durante a gravação, por quanto tempo ela executou e de onde ela foi chamada — Mozilla

Obrigado por ter lido até aqui, se você gostou do post, manda um 💚 e compartilha no Twitter! Valeu! 🙏🏼



One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.