Entendendo o comportamento de renderização no React
Publicados: 2020-11-16Junto com vida, morte, destino e impostos, o comportamento de renderização do React é uma das maiores verdades e mistérios da vida.
Vamos mergulhar!
Como todo mundo, comecei minha jornada de desenvolvimento front-end com jQuery. A manipulação pura do DOM baseada em JS era um pesadelo na época, então era o que todo mundo estava fazendo. Então, lentamente, os frameworks baseados em JavaScript tornaram-se tão proeminentes que não pude mais ignorá-los.
O primeiro que aprendi foi o Vue. Eu tive um tempo incrivelmente difícil porque componentes e estado e tudo mais era um modelo mental totalmente novo, e era muito doloroso encaixar tudo. Mas, eventualmente, eu consegui, e me bati nas costas. Parabéns, amigo, disse a mim mesmo, você fez a subida íngreme; agora, o restante dos frameworks, caso você precise aprendê-los, será muito fácil.
Então, um dia, quando comecei a aprender React, percebi o quão terrivelmente errado eu estava. O Facebook não facilitou as coisas colocando Hooks e dizendo a todos: “Ei, use isso de agora em diante. Mas não reescreva as classes; as aulas estão bem. Na verdade, nem tanto, mas tudo bem. Mas Hooks são tudo, e eles são o futuro.
Entendi? Excelente!".
Eventualmente, eu cruzei aquela montanha também. Mas então fui atingido por algo tão importante e difícil quanto o próprio React: renderizar .

Se você já se deparou com renderização e seus mistérios no React, sabe do que estou falando. E se você não tem, você não tem ideia do que está reservado para você!
Mas antes de perder tempo com qualquer coisa, é um bom hábito perguntar o que você ganharia com isso (ao contrário de mim, que sou um idiota superexcitado e que aprenderá qualquer coisa com prazer apenas por isso). Se sua vida como um desenvolvedor React está indo bem sem se preocupar com o que é essa renderização, por que se importar? Boa pergunta, então vamos responder isso primeiro, e então veremos o que realmente é renderização.
Por que entender o comportamento de renderização no React é importante?
Todos nós começamos a aprender React escrevendo (atualmente, funcionais) componentes que retornam algo chamado JSX. Também entendemos que esse JSX é de alguma forma convertido em elementos HTML DOM reais que aparecem na página. As páginas são atualizadas à medida que o estado é atualizado, as rotas mudam conforme o esperado e está tudo bem. Mas essa visão de como o React funciona é ingênua e uma fonte de muitos problemas.
Embora muitas vezes tenhamos sucesso em escrever aplicativos completos baseados em React, há momentos em que encontramos certas partes de nosso aplicativo (ou o aplicativo inteiro) notavelmente lentos. E a pior parte. . . não temos uma única pista do porquê! Fizemos tudo corretamente, não vemos erros ou avisos, seguimos todas as boas práticas de design de componentes, padrões de codificação, etc., e nenhuma lentidão de rede ou computação lógica de negócios cara está acontecendo nos bastidores.
Às vezes, é um problema totalmente diferente: não há nada de errado com o desempenho, mas o aplicativo se comporta de maneira estranha. Por exemplo, fazer três chamadas de API para o back-end de autenticação, mas apenas uma para todas as outras. Ou algumas páginas estão sendo redesenhadas duas vezes, com a transição visível entre as duas renderizações da mesma página criando um UX chocante.

Pior de tudo, não há ajuda externa disponível em casos como esses. Se você for ao seu fórum de desenvolvimento favorito e fizer essa pergunta, eles responderão: “Não dá para saber sem olhar para o seu aplicativo. Você pode anexar um exemplo mínimo de trabalho aqui?” Bem, é claro que você não pode anexar o aplicativo inteiro por motivos legais, enquanto um pequeno exemplo de trabalho dessa parte pode não conter esse problema porque não está interagindo com todo o sistema da maneira que está no aplicativo real.
Parafusado? Sim, se você me perguntar.
Então, a menos que você queira ver esses dias de aflição, sugiro que desenvolva um entendimento – e interesse, devo insistir; a compreensão obtida com relutância não o levará muito longe no mundo React - nessa coisa mal compreendida chamada renderização em React. Confie em mim, não é tão difícil de entender, e embora seja muito difícil de dominar, você irá muito longe sem ter que conhecer todos os cantos e recantos.
O que significa renderização em React?
Essa, meu amigo, é uma excelente pergunta. Nós não costumamos perguntar isso quando aprendemos React (eu sei porque não fiz) porque a palavra “render” talvez nos leve a uma falsa sensação de familiaridade. Embora o significado do dicionário seja completamente diferente (e não é importante nesta discussão), nós programadores já temos uma noção do que deve significar. Trabalhar com telas, APIs 3D, placas gráficas e ler especificações de produtos treina nossas mentes para pensar em algo como “pintar uma imagem” quando lemos a palavra “renderizar”. Na programação do mecanismo de jogo, há um Renderer, cujo único trabalho é – precisamente!, pintar o mundo conforme entregue pela Cena.
E então pensamos que quando o React “renderiza” algo, ele coleta todos os componentes e redesenha o DOM da página web. Mas no mundo React (e sim, mesmo na documentação oficial), não é disso que se trata a renderização. Então, vamos apertar os cintos e dar um mergulho bem profundo nas partes internas do React.

Você deve ter ouvido que o React mantém o que é chamado de DOM virtual e que periodicamente o compara com o DOM real e aplica as alterações necessárias (é por isso que você não pode simplesmente jogar jQuery e React juntos - o React precisa assumir o controle total de o DOM). Agora, esse DOM virtual não é composto de elementos HTML como o DOM real, mas de elementos React. Qual é a diferença? Boa pergunta! Por que não criar um pequeno aplicativo React e ver por nós mesmos?
Eu criei este aplicativo React muito simples para essa finalidade. O código inteiro é apenas um único arquivo contendo algumas linhas:
import React from "react"; import "./styles.css"; export default function App() { const element = ( <div className="App"> <h1>Hello, there!</h1> <h2>Let's take a look inside React elements</h2> </div> ); console.log(element); return element; }
Observe o que estamos fazendo aqui?
Sim, simplesmente registrando a aparência de um elemento JSX. Essas expressões e componentes JSX são algo que escrevemos centenas de vezes, mas raramente prestamos atenção ao que está acontecendo. Se você abrir o console de desenvolvimento do seu navegador e executar este aplicativo, verá um Object
que se expande para:

Isso pode parecer intimidante, mas observe alguns detalhes interessantes:
- O que estamos vendo é um objeto JavaScript simples e regular e não um nó DOM.
- Observe que a propriedade
props
diz que tem umclassName
deApp
(que é a classe CSS definida no código) e que esse elemento tem dois filhos (isso também corresponde, sendo os elementos filhos as tags<h1>
e<h2>
) . - A propriedade
_source
nos diz onde o código-fonte inicia o corpo do elemento. Como você pode ver, ele nomeia o arquivoApp.js
como a fonte e menciona a linha número 6. Se você olhar o código novamente, verá que a linha 6 está logo após a tag JSX de abertura, o que faz sentido. Os parênteses JSX contêm o elemento React; eles não fazem parte disso, pois servem para se transformar em uma chamadaReact.createElement()
posteriormente. - A propriedade
__proto__
nos diz que este objeto deriva todos os seus. properties da raiz JavaScriptObject
, novamente reforçando a ideia de que são apenas objetos JavaScript comuns que estamos vendo aqui.
Então, agora, entendemos que o chamado DOM virtual não se parece em nada com o DOM real, mas é uma árvore de objetos React (JavaScript) representando a interface do usuário naquele momento.

Exausta?
Confie em mim, eu também estou. Revirar essas ideias várias vezes na minha cabeça para tentar apresentá-las da melhor maneira possível e depois pensar nas palavras para trazê-las à tona e reorganizá-las – não é fácil.
Mas estamos nos distraindo!
Tendo sobrevivido até aqui, agora estamos em condições de responder à pergunta que procurávamos: o que é renderização em React?
Bem, renderização é o processo do mecanismo React percorrendo o DOM virtual e coletando o estado atual, adereços, estrutura, alterações desejadas na interface do usuário, etc. O React agora atualiza o DOM virtual usando alguns cálculos e também compara o novo resultado com o DOM real na página. Esse cálculo e comparação é o que a equipe do React chama oficialmente de “reconciliação”, e se você estiver interessado em suas ideias e algoritmos relevantes, você pode verificar os documentos oficiais.
Hora de se comprometer!
Uma vez finalizada a parte de renderização, o React inicia uma fase chamada “commit”, durante a qual aplica as alterações necessárias no DOM. Essas alterações são aplicadas de forma síncrona (uma após a outra, embora um novo modo que funcione simultaneamente seja esperado em breve) e o DOM é atualizado. Exatamente quando e como o React aplica essas mudanças não é nossa preocupação, pois é algo que está totalmente oculto e provavelmente continuará mudando à medida que a equipe do React experimenta coisas novas.
Renderização e desempenho em aplicativos React
Já entendemos que renderização significa coletar informações e não precisa resultar em alterações visuais do DOM todas as vezes. Também sabemos que o que consideramos como “renderização” é um processo de duas etapas envolvendo renderização e confirmação. Veremos agora como a renderização (e mais importante, a rerenderização) é acionada em aplicativos React e como não conhecer os detalhes pode fazer com que os aplicativos tenham um desempenho ruim.
Re-renderização devido a alteração no componente pai
Se um componente pai no React mudar (digamos, porque seu estado ou props mudaram), o React percorre toda a árvore desse elemento pai e renderiza novamente todos os componentes. Se o seu aplicativo tiver muitos componentes aninhados e muitas interações, você estará, sem saber, tendo um grande impacto no desempenho toda vez que alterar o componente pai (supondo que seja apenas o componente pai que você deseja alterar).

É verdade que a renderização não fará com que o React altere o DOM real porque, durante a reconciliação, ele detectará que nada mudou para esses componentes. Mas ainda é tempo de CPU e memória desperdiçados, e você ficaria surpreso com a rapidez com que isso aumenta.
Re-renderização devido a mudança no contexto
O recurso Context do React parece ser a ferramenta de gerenciamento de estado favorita de todos (algo para o qual não foi construído). É tudo muito conveniente - apenas envolva o componente superior no provedor de contexto e o resto é uma questão simples! A maioria dos aplicativos React estão sendo construídos assim, mas se você leu este artigo até agora, provavelmente viu o que está errado. Sim, toda vez que o objeto de contexto é atualizado, ele aciona uma re-renderização massiva de todos os componentes da árvore.
A maioria dos aplicativos não tem consciência de desempenho, então ninguém percebe, mas, como dito antes, esses descuidos podem ser muito caros em aplicativos de alto volume e alta interação.
Melhorando o desempenho de renderização do React
Então, diante de tudo isso, o que podemos fazer para melhorar o desempenho de nossos aplicativos? Acontece que há algumas coisas que podemos fazer, mas tome nota de que discutiremos apenas no contexto de componentes funcionais. Componentes baseados em classe são altamente desencorajados pela equipe do React e estão saindo.
Use Redux ou bibliotecas semelhantes para gerenciamento de estado
Aqueles que amam o mundo rápido e sujo do Context tendem a odiar o Redux, mas essa coisa é muito popular por boas razões. E uma dessas razões é o desempenho - a função connect()
no Redux é mágica, pois (quase sempre) renderiza corretamente apenas os componentes necessários. Sim, basta seguir a arquitetura padrão do Redux e o desempenho é gratuito. Não é nenhum exagero que se você adotar a arquitetura Redux, você evita a maioria dos problemas de desempenho (e outros) imediatamente.
Use memo()
para “congelar” componentes
O nome “memo” vem de Memoization, que é um nome chique para cache. E se você não encontrou muito cache, tudo bem; aqui está uma descrição diluída: toda vez que você precisa de algum resultado de computação/operação, você olha no lugar onde estava mantendo os resultados anteriores; se encontrar, ótimo, simplesmente retorne esse resultado; se não, vá em frente e execute essa operação/computação.
Antes de mergulhar direto no memo()
, vamos primeiro ver como a renderização desnecessária ocorre no React. Começamos com um cenário simples: uma pequena parte da interface do usuário do aplicativo que mostra ao usuário quantas vezes ele gostou do serviço/produto (se você está tendo problemas para aceitar o caso de uso, pense em como no Medium você pode “bater palmas ” várias vezes para mostrar o quanto você apoia/curte um artigo).
Há também um botão que permite aumentar as curtidas em 1. E, finalmente, há outro componente interno que mostra aos usuários os detalhes básicos da conta. Não se preocupe se estiver achando isso difícil de seguir; Agora, fornecerei código passo a passo para tudo (e não há muito disso) e, no final, um link para um playground onde você pode mexer no aplicativo em funcionamento e melhorar sua compreensão.
Vamos primeiro abordar o componente sobre informações do cliente. Vamos criar um arquivo chamado CustomerInfo.js
que contém o seguinte código:
import React from "react"; export const CustomerInfo = () => { console.log("CustomerInfo was rendered! :O"); return ( <React.Fragment> <p>Name: Sam Punia</p> <p>Email: [email protected]</p> <p>Preferred method: Online</p> </React.Fragment> ); };
Nada extravagante, certo?
Apenas algum texto informativo (que poderia ter sido passado através de adereços) que não se espera que mude à medida que o usuário interage com o aplicativo (para os puristas por aí, sim, claro que pode mudar, mas o ponto é, quando comparado ao resto do aplicativo, é praticamente estático). Mas observe a instrução console.log()
. Esta será nossa pista para saber que o componente foi renderizado (lembre-se, “renderizado” significa que suas informações foram coletadas e calculadas/comparadas, e não que foram pintadas no DOM real).
Portanto, durante nossos testes, se não virmos essa mensagem no console do navegador, nosso componente não foi renderizado; se o vemos aparecer 10 vezes, significa que o componente foi renderizado 10 vezes; e assim por diante.
E agora vamos ver como nosso componente principal usa esse componente de informações do cliente:
import React, { useState } from "react"; import "./styles.css"; import { CustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <CustomerInfo /> </div> </div> ); }
Assim, vemos que o componente App
possui um estado interno gerenciado por meio do gancho useState()
. Esse estado continua contando quantas vezes o usuário gostou do serviço/site e é inicialmente definido como zero. Nada desafiador no que diz respeito aos aplicativos React, certo? No lado da interface do usuário, as coisas se parecem com isso:

O botão parece tentador demais para não ser esmagado, pelo menos para mim! Mas antes de fazer isso, vou abrir o console de desenvolvimento do meu navegador e limpá-lo. Depois disso, vou apertar o botão algumas vezes, e aqui está o que vejo:

Apertei o botão 19 vezes e, como esperado, a contagem total de curtidas é de 19. O esquema de cores estava dificultando muito a leitura, então adicionei uma caixa vermelha para destacar o principal: o componente <CustomerInfo />
foi renderizado 20 vezes!
Por que 20?
Uma vez quando tudo foi renderizado inicialmente, e então, 19 vezes quando o botão foi pressionado. O botão altera totalLikes
, que é um pedaço de estado dentro do componente <App />
e, como resultado, o componente principal é renderizado novamente. E como aprendemos nas seções anteriores deste post, todos os componentes dentro dele também são renderizados novamente. Isso é indesejado porque o componente <CustomerInfo />
não mudou no processo e ainda contribuiu para o processo de renderização.
Como podemos prevenir isso?
Exatamente como o título desta seção diz, usando a função memo()
para criar uma cópia “preservada” ou em cache do componente <CustomerInfo />
. Com um componente memoizado, o React olha suas props e as compara com as props anteriores, e se não houver nenhuma mudança, o React não extrai uma nova saída “render” deste componente.
Vamos adicionar esta linha de código ao nosso arquivo CustomerInfo.js
:
export const MemoizedCustomerInfo = React.memo(CustomerInfo);
Sim, isso é tudo que precisamos fazer! Agora é hora de usar isso em nosso componente principal e ver se algo muda:
import React, { useState } from "react"; import "./styles.css"; import { MemoizedCustomerInfo } from "./CustomerInfo"; export default function App() { const [totalLikes, setTotalLikes] = useState(0); return ( <div className="App"> <div className="LikesCounter"> <p>You have liked us {totalLikes} times so far.</p> <button onClick={() => setTotalLikes(totalLikes + 1)}> Click here to like again! </button> </div> <div className="CustomerInfo"> <MemoizedCustomerInfo /> </div> </div> ); }
Sim, apenas duas linhas foram alteradas, mas eu queria mostrar todo o componente de qualquer maneira. Nada mudou em relação à interface do usuário, então se eu pegar a nova versão e apertar o botão curtir algumas vezes, recebo isso:

Então, quantas mensagens de console nós temos?
Apenas um! Isso significa que, além da renderização inicial, o componente não foi tocado. Imagine os ganhos de desempenho em um aplicativo realmente de alta escala! Ok, ok, o link para o playground de código que prometi está aqui. Para replicar o exemplo anterior, você precisará importar e usar CustomerInfo
em vez de MemoizedCustomerInfo
de CustomerInfo.js
.
Dito isso, memo()
não é areia mágica que você pode espalhar em todos os lugares e esperar resultados mágicos. O uso excessivo de memo()
também pode introduzir bugs complicados em seu aplicativo e, às vezes, simplesmente fazer com que algumas atualizações esperadas falhem. O conselho geral sobre otimização “prematura” também se aplica aqui. Primeiro, construa seu aplicativo conforme sua intuição diz; então, faça alguns perfis intensivos para ver quais partes são lentas e se parecer que os componentes memorizados são a solução certa, só então introduza isso.
Design de componentes “inteligentes”
Coloco “inteligente” entre aspas porque: 1) A inteligência é altamente subjetiva e situacional; 2) Ações supostamente inteligentes geralmente têm consequências desagradáveis. Então, meu conselho para esta seção é: não seja muito confiante no que você está fazendo.
Com isso fora do caminho, uma possibilidade de melhorar o desempenho de renderização é projetar e posicionar os componentes de maneira um pouco diferente. Por exemplo, um componente filho pode ser refatorado e movido para algum lugar acima na hierarquia para escapar de re-renderizações. Nenhuma regra diz que “o componente ChatPhotoView deve estar sempre dentro do componente Chat”. Em casos especiais (e esses são os casos em que temos evidências baseadas em dados de que o desempenho está sendo afetado), dobrar/quebrar as regras pode ser uma ótima ideia.
Conclusão
Muito mais pode ser feito para otimizar aplicativos React em geral, mas como este artigo é sobre Renderização, restringi o escopo da discussão. Independentemente disso, espero que agora você tenha uma visão melhor do que está acontecendo no React nos bastidores, o que realmente é a renderização e como isso pode afetar o desempenho do aplicativo.
A seguir, vamos entender o que é React Hooks?