Artigo original: How to identify and resolve wasted renders in React

Escrito por: Nayeem Reza

Recentemente, estive analisando o desempenho de uma aplicação do React na qual eu trabalhava e, de repente, pensei em definir algumas métricas de desempenho para essa aplicação. Com isso, descobri que a primeira coisa que precisaria resolver são as renderizações desnecessárias que estou fazendo em cada uma das páginas da web. Você pode estar se perguntando o que são renderizações desnecessárias, não é mesmo? Vamos aprofundar nisso.

Desde o começo, o React mudou toda a filosofia de criação de aplicações para a web e, consequentemente, o modo como pessoas que desenvolvem o front-end pensam. Com a introdução do Virtual DOM, o React torna as atualizações de interface do usuário mais eficiente e, com isso, a experiência da aplicação para a web se torna mais "limpa".

Você já se perguntou como deixar suas aplicações em React mais rápida? Por que aplicações para a web em React de tamanho moderado continuam tendo um desempenho ruim? O problema está no modo como utilizamos o React!

Como o React funciona

Uma biblioteca moderna de front-end como o React não torna nossa aplicação magicamente mais rápida. Primeiro, nós, pessoas desenvolvedoras, devemos entender como o React funciona. Como os componentes atravessam os ciclos de vida ao longo da existência da aplicação? Portanto, antes de mergulharmos em qualquer técnica de otimização, precisamos entender melhor como o React realmente funciona nos bastidores.

Na essência do React, temos a sintaxe do JSX e a poderosa habilidade do React de construir e comparar Virtual DOMs (texto em inglês). Desde o seu lançamento, o React influenciou muitas outras bibliotecas de front-end. Por exemplo, o Vue.js também depende da ideia de Virtual DOMs.

Cada aplicação do React começa com um componente raiz. Podemos pensar na aplicação inteira como uma estrutura em forma de árvore onde cada galho é um componente. Componentes no React são 'funções' que renderizam a interface do usuário com base nos dados, ou seja, nas props e no state. Podemos representar com: CF (componente funcional)

UI = CF(dados)

Usuários interagem com a interface e causam alterações nos dados. As interações são tudo que o usuário pode fazer na nossa aplicação. Exemplos disso são: clicar em um botão, ver um carrossel de imagens, fazer solicitações a APIs. Todas essas interações alteram apenas os dados. Elas nunca causam nenhuma alteração na interface.

Aqui, os dados desempenham um papel fundamental na definição do estado de uma aplicação. Não apenas o que armazenamos em nosso banco de dados. Mesmo diferentes estados na interface do usuário, como qual aba está atualmente selecionada ou se uma caixa de seleção está marcada ou não, fazem parte desses dados. Sempre que ocorre uma alteração nos dados, o React utiliza as funções do componente para recriar a interface do usuário, mas apenas virtualmente:

UI1 = CF(dados1)
UI2 = CF(dados2)

O React calcula a diferença entre a interface atual e a nova interface aplicando um algoritmo de comparação (documentação em inglês) nas duas versões de seu Virtual DOM.

Mudancas = Diferenca(UI1, UI2)
Nota da tradução: caso tente acessar a documentação apontada no parágrafo acima e mais abaixo, você verá que os links, agora, apontam para uma página de documentação legada. Ocorreram diversas mudanças tanto no React quanto em sua documentação a partir da versão 18, mas a explicação sobre o algoritmo de comparação ainda é válida para o entendimento do processo.

Em seguida, o React prossegue aplicando apenas as alterações na interface do usuário (UI) real no navegador. Quando os dados associados a um componente mudam, o React determina se uma atualização na DOM "real" é necessária. Isso permite ao React evitar operações demoradas e que consomem muitos recursos no navegador, como a criação de elementos no DOM e a interação com elementos já existentes quando isso não é estritamente necessário.

Essa diferenciação e renderização repetida de componentes pode ser uma das principais fontes de problemas de desempenho no React. Criar uma aplicação em React na qual o algoritmo de diferenciação não consegue conciliar (documentação em inglês) efetivamente, leva a uma renderização repetida de toda a aplicação, na verdade, resultando em renderizações desperdiçadas e podendo resultar em uma experiência lenta e frustrante.

Durante o processo de renderização inicial, o React constrói uma árvore DOM como esta:

y4qB7PH40s9RK02BE5njs2w-lPMJVrKtqqor

Suponha que uma parte dos dados mude. O que queremos que o React faça é renderizar novamente apenas os componentes que são diretamente afetados por essa mudança específica. Possivelmente, queremos que pule até mesmo o processo de diferenciação para o restante dos componentes. Vamos supor que alguns dados mudam no Componente 2 na imagem acima, e que esses dados foram passados de R para B e, depois, para 2. Se R for renderizado novamente, ele também renderizará outra vez cada um de seus filhos (A, B, C e D). O que o React realmente faz é o seguinte:

2X1HZDWVjVFi0fib0I9qUEEyeFIWLnBDcolg

Na imagem acima, todos os nós amarelos são renderizados e diferenciados. Isso resulta em desperdício de tempo/recursos de computação. Aqui é onde concentrarmos principalmente nossos esforços de otimização. Configurando cada componente para renderizar e diferenciar apenas quando for necessário. Isso nos permitirá recuperar aqueles ciclos de CPU desperdiçados. Primeiro, veremos como identificar renderizações desnecessárias na nossa aplicação.

Identificar renderizações desnecessárias

Existem algumas maneiras diferentes de se fazer isso. O método mais simples é ativar a opção de atualizações de destaque na preferência de ferramentas de desenvolvimento do React.

Rjxm5xSv-yo4igzgz4OhGUsyRobsIjnJMCTq

Durante a interação com a sua aplicação, as atualizações são destacadas na tela com bordas coloridas. Você verá os componentes que foram renderizados novamente. Isso nos permite identificar novas renderizações que não eram necessárias.

Vamos seguir este exemplo:

sEbzC97cYGA21Sap7VGKTlwNZ5OEBCDZwzD5

Observe que, quando inserimos uma segunda tarefa, a primeira tarefa também pisca na tela a cada tecla pressionada. Isso significa que ela está sendo renderizada novamente pelo React juntamente com a entrada. Chamamos esse acontecimento de renderização "desnecessária". Sabemos que é desnecessária porque o conteúdo da primeira tarefa não mudou, mas o React não sabe disso.

Mesmo que o React atualize apenas os nós do DOM que foram alterados, a nova renderização ainda leva algum tempo. Em muitos casos, isso não é um problema, mas, se a lentidão for perceptível, devemos considerar algumas coisas para impedir essas renderizações redundantes.

Usando o método shouldComponentUpdate

Por padrão, O React renderizará o virtual DOM e comparará a diferença de cada componente na árvore para qualquer alteração em suas props ou state. Isso, no entanto, não é uma boa opção. À medida que nossa aplicação cresce, tentar renderizar novamente e comparar toda virtual DOM em cada ação acabará tornando tudo mais lento.

O React, então, fornece um método de ciclo de vida simples para indicar se um componente precisa de uma nova renderização. Isso é feito através de shouldComponentUpdate, que é acionado antes do início do processo da nova renderização. A implementação padrão dessa função retorna true.

M8-a8KtHWAtoicHp8-mmVkEd-FX-gqwZw6Co

Quando essa função retorna true para qualquer componente, ela permite que o processo de diferenciação de renderização seja acionado. Isso nos dá o poder de controlar esse processo de diferenciação. Suponha que precisemos evitar que um componente seja renderizado novamente, precisamos apenas retornar false dessa função. Como podemos ver na implementação do método, podemos comparar as props e state atuais e próximos para determinar se uma nova renderização é necessária.

5ERuL7iT4wDYhPCj9mmhSwp-cCch6ydPdNMJ

Uso de componentes puros

Você já deve conhecer o React.Component, mas e React.PureComponent? Nós já falamos aqui sobre o método de ciclo de vida shouldComponentUpdate. Em componentes puros, já existe uma implementação padrão de shouldComponentUpdate() com uma comparação rasa de props e state. Portanto, um componente puro é um componente que apenas é renderizado novamente se as props e o state forem diferentes dos anteriores.

jQnYm0ejA57POOcjmOJ1fauWWC7kB0q-y7Dr
Na comparação rasa, tipos de dados primitivos como string, booleano e número são comparados pelo seu valor, enquanto tipos de dados complexos como array, objeto e função são comparados pela referência.

Se tivermos um componente funcional sem state em que precisamos implementar esse método de comparação antes que cada nova renderização ocorra, como podemos fazer isso? O React possui um Componente de Ordem Superior, chamado React.memo. É semelhante ao React.PureComponent, mas para componentes funcionais em vez de classes.

A1MmnhyMmXvz-kwKx6619gJVx7IfUwMJGSKR

Por padrão, ele faz o mesmo que o shouldComponentUpdate(), que apenas compara superficialmente o objeto de props. Se, contudo, quisermos ter o controle sobre essa comparação, o que faremos? Também podemos fornecer uma função de comparação customizada como segundo argumento.

38vWplvDxFSEpjSeL72vcNaLRs7XzBYwa1xI

Tornar os dados imutáveis


Poderíamos usar um React.PureComponent e ainda ter uma maneira eficiente de detectar automaticamente quando qualquer prop complexa ou state – como um array, objeto ou outras opções – fossem alterados? É aqui que a estrutura de dados imutáveis torna a vida mais fácil.

A ideia por trás do uso de estruturas de dados imutáveis é simples. Como falamos anteriormente, para tipos de dados complexos, a comparação é feita com base em suas referências. Sempre que um objeto contendo dados complexos é alterado, em vez de fazer as alterações nesse objeto, podemos criar uma cópia desse objeto com as alterações, o que criará uma referência.

O ES6 possui o operador de spread que torna isso possível.

CbPn9o3eE53eh784JIzXPwaS7oqOefb-wW-O

Podemos fazer o mesmo com arrays:

n73IBFev-5etKfGYTOAH-qAs8R3E6EICcaSv

Evite passar uma nova referência para os mesmos dados antigos

Sabemos que sempre que props de um componente mudam, uma nova renderização acontece. Às vezes, porém, as props não mudam. Escrevemos o código de um modo que o React pensa que mudou, e isso acaba causando uma nova renderização. Dessa vez, no entanto, é uma renderizacão desnecessária. Então, basicamente, precisamos garantir que estamos passando uma referência diferente como props para dados diferentes. Além disso, precisamos evitar passar uma nova referência para os mesmos dados. Agora, vamos analisar alguns casos em que estamos criando esse problema. Veja o código abaixo:

qDjrVvrQAPlavtw0rQGE05jPWXFvP8unpt9n

Aqui está o conteúdo do componente BookInfo, onde estamos renderizando dois componentes, BookDescription e BookReview. Esse é o código correto e funciona bem, mas há um problema. BookDescription será renderizado novamente sempre que recebermos novos dados de avaliações como props. Por quê? Assim que o componente BookInfo recebe novas props, a função render é chamada para criar a sua árvore de elementos. A função de renderização cria uma constante book, o que significa criar outra referência. Portanto, BookDescription receberá book como uma nova referência, o que causará uma nova renderização de BookDescription. Para resolver isso, vamos refatorar esse código do seguinte modo:

ushbIP1Vt8uV63TFX6DGyQAQZpIoNiW7BzPg

Agora, a referência é sempre a mesma, this.book. Um outro objeto não é criado durante a renderização. Essa filosofia de nova renderização se aplica a todas as props, incluindo eventos, como abaixo:

xmUdrbVqtYaa37e1Ds4ykJRiZRzpsQweDUVz

Aqui, usamos duas maneiras diferentes (vincular métodos e usar uma arrow function na renderização) para chamar os métodos manipuladores de eventos, mas ambas criarão uma função sempre que o componente for renderizado novamente. Para resolver esses problemas, podemos vincular o método no constructor e usar propriedades de classe, o que ainda é experimental e não padronizado, mas muitos desenvolvedores já estão usando para passar funções para outros componentes em aplicações prontas para produção:

wTadfqSaGeRy7nH-jSt2-285fbZGi2Zoy9rH

Conclusão

Internamente, o React utiliza diversas técnicas inteligentes para minimizar a quantidade de operações custosas no DOM e necessárias para atualizar a interface do usuário. Para muitas aplicações, o uso do React resultará em uma interface de usuário rápida sem a necessidade de fazer muito trabalho para otimizar o desempenho especificamente. No entanto, se conseguirmos seguir as técnicas que mencionei acima para resolver as renderizações desnecessárias, então, para aplicações grandes, também teremos uma experiência muito fluida em termos de desempenho.