Como evitar renderizações desnecessárias com React.memo
Como evitar renderizações desnecessárias com React.memo
Como desenvolvedores React, sabemos que uma boa prática é a abstração do nosso código em componentes. Ou seja, partes de código isoladas que podem ser reaproveitadas em vários outros lugares da nossa aplicação. Evitando, assim, a repetição de código. Porém, muitas vezes, fazemos a renderização dessas partes isoladas de código de forma desnecessária. Você sabe como evitar renderizações desnecessárias? Neste artigo, vamos aprender o passo a passo.
Muitas vezes, realizamos essa renderização desnecessária sem perceber! Isto é, prejudicando a performance da nossa aplicação. Para entender melhor como evitar renderizações desnecessárias, precisamos primeiro conhecer o conceito de Reconciliation do React.
O que é Reconciliation?
Toda vez que um componente sofre alteração em seu estado ou propriedade (ou um componente pai também sofre esses tipos de alterações), ele entra em um fluxo de reconciliação. Ou seja, é realizada uma nova renderização desse componente.
Você deve estar se perguntando: “e como acontece essa renderização?”
Ao cair em um dos casos citados acima, é gerado uma nova DOM (árvore de elementos, basicamente, o HTML desse componente) e é feita uma comparação com a DOM já existente. Caso haja alguma diferença entre essas árvores de elementos, é realizada uma substituição da DOM antiga pela nova DOM, que contém as alterações.
Uma leitura mais avançada sobre esse conceito pode ser feito através deste link.
E quando acontece o problema de renderização desnecessária?
Esse processo de reconciliação do React é muito rápido. Porém, há momentos em que ele pode acontecer 100/150 vezes, só que desnecessariamente. Principalmente quando temos informações muito grandes, como listas ou componentes em uma página que sofra alterações em seu estado rapidamente. Para entender melhor, vamos à prática.
Criando a aplicação React
O primeiro passo é criarmos um projeto React normalmente. No meu caso, utilizarei o Yarn para a criação do projeto, mas você pode utilizar o método que preferir. No terminal, digitamos o seguinte comando:
O processo de criação do projeto e de instalação das dependências será iniciado e, após finalizados, devemos abrir o projeto em um editor de código. Aqui, vamos utilizar o VSCode.
Esse projeto será uma aplicação de listagem de posts. Assim, podemos deixar nosso projeto com a seguinte estrutura de pastas:
Existirão dois componentes nessa aplicação. O primeiro será o ‘PostList’, responsável por buscar os posts na API e listá-los. O segundo será o ‘PostItem’, responsável por receber cada post e tratá-lo. Ou seja, definir como e o que será mostrado. A API que utilizaremos para buscar os posts é a JSONPlaceHolder.
Nela, podemos acessar o endpoint ‘/posts’ que nos retornará 100 posts com ‘title’, ‘body’ e algumas outras informações.
A seguir, seguem as estruturas dos componentes citados:
PostItem
Nesse componente, receberemos o post através da prop ‘posts’ e retornaremos uma estrutura com o título e o conteúdo do post.
PostList
Nesse componente, buscaremos os posts em um useEffect, usando a FetchAPI. Colocaremos esses posts em um estado e faremos um ‘map’ desses posts, passando cada um para o nosso componente PostItem através de prop ‘post’ que definimos.
Após criarmos esses componentes, devemos alterar o arquivo ‘App.js’ e deixá-lo com a seguinte estrutura:
Perceba que a estrutura de pastas do nosso projeto foi alterada. Agora temos a pasta ‘components’ contendo os dois componentes que criamos.
Feito isso, ao executarmos o projeto (se você estiver usando o Yarn, para rodar o projeto, só é preciso digitar ‘yarn start’ no terminal, dentro da pasta do projeto), devemos obter o seguinte resultado no navegador:
Identificando possíveis problemas de performance
Existe uma extensão para o Chrome chamada ‘React Developer Tools’. Através dela, temos acesso a recursos para inspecionar aplicações em React pelo browser. Então, com essa extensão instalada, podemos clicar na tela do browser com o botão direito do mouse para abrir um menu de opções, entre as quais devemos escolher ‘Inspecionar’. Ao selecionar essa opção, aparecerá uma área, na qual veremos, nas últimas opções do header dessa área, os itens ‘Components’ e ‘Profiler’. Em ‘Components’, podemos visualizar os componentes da nossa aplicação, e em ‘Profiler’, está o nosso tesouro.
Ao selecionar ‘Profiler’, será aberto um espaço onde podemos visualizar algumas informações sobre a nossa aplicação. Dentre elas, o processo de renderização dos componentes. Para isso, basta clicar no sinal de ‘reloading’, segundo ícone do menu desse ambiente (isso fará a aplicação recarregar, mas agora sendo monitorada). Após o reload da aplicação, podemos clicar na bolinha preenchida que está ao lado esquerdo desse mesmo símbolo de ‘reloading’.
O que acontece aqui ? Basicamente, ele vai fazer o reload da aplicação e vai trazer algumas informações que acontecem durante a renderização dos componentes. Dentre as informações dessa renderização, está o tempo de renderização e os componentes renderizados:
Nessa imagem, podemos ver que o nosso component ‘App’ foi renderizado, depois o nosso componente ‘PostList’ foi renderizado (demorando 33.7ms) e, depois, o componente ‘PostItem’ (em ciano) foi renderizado 100x, pois temos 100 posts sendo retornados da API.
Criando problema de performance
Até agora não temos nada de anormal, pois o que acontece é o fluxo normal do React. Ou seja, a renderização dos componentes de acordo com as necessidades da aplicação. Porém, existirá uma nova funcionalidade em nossa aplicação. A de adição de um novo post. Como o objetivo é alterar um estado do componente para que ele seja renderizado novamente, não criarei toda a lógica para adição de um post, mas sim apenas um ‘input’, simulando a inserção do título do novo post.
Para isso, no componente ‘PostList’, criaremos um estado para armazenar o valor do input, criaremos o input em si e, no ‘onChange’ desse input, colocaremos uma função que altera o valor do estado, inserindo sempre o texto atual do input:
Perceba que agora temos o input em si, acima da lista, com a função ‘onChange’ alterando o valor do estado para o texto digitado no input. Também definimos o valor do input como sempre sendo o valor do estado.
Agora, em nossa aplicação, deve existir um campo de input bem no topo da lista, como demonstrado na imagem abaixo:
Aprendemos, no início desse post, sobre o conceito de reconciliação: sempre que um componente ou seu componente pai sofrer alterações em seus estados ou propriedades, ele será renderizado novamente. Então, se digitarmos algum texto no input, o estado do componente ‘PostList’ irá mudar, esse mesmo componente será renderizado novamente e, por ser um componente pai do ‘PostItem’, fará com que este último também seja renderizado novamente.
Renderização desnecessária
Podemos verificar isso fazendo o mesmo processo de inspeção na nossa aplicação. Só que, após o reload pelo menu, digitamos algo no input e aí sim clicamos na bolinha preenchida. Teremos o seguinte retorno:
Cada quadrado amarelo, no canto superior direito, significa uma renderização da nossa aplicação. Ao clicarmos neles, veremos que o componente ‘PostItem’ foi renderizado 100 vezes a cada alteração de letra que fizemos no input, ou seja, a cada renderização. Isso devido ao componente pai ‘PostList’ ter sofrido alteração em seu estado.
Mas, por que o componente ‘PostItem’ teve que ser renderizado 100 vezes a cada alteração do seu componente pai se ele (o PostItem) não teve nenhum estado ou propriedade alterada e a modificação feita no componente pai (inserção em um input) não interfere em nada no componente filho? Isso só fez com que nossa aplicação demorasse mais para carregar e exigisse mais do cliente que roda a aplicação.
Em um dispositivo com um bom hardware, situações como essas podem passar despercebidas. Entretanto, para aqueles que tem um hardware fraco, isso pode causar uma má experiência, principalmente se o sistema for muito grande e existirem muitas situações como essas ocorrendo.
Veja também:
Como evitar renderizações desnecessárias: resolvendo o problema de performance
É aí que entra o React.memo. Para evitar renderizações desnecessárias, de forma resumida, ele ‘memoriza’ um componente e só renderiza-o caso ele tenha alguma propriedade ou estado alterado. Ou seja, é retirada a regra de ter que ser renderizado novamente devido a alterações no componente pai.
Devemos usar o React.memo no componente que está sendo renderizado de forma desnecessária. Neste caso, o componente será o PostItem, pois o PostList está sendo renderizado novamente por conta das alterações em seu estado, o que é correto. Usamos o ‘memo’ da seguinte forma:
Importei o ‘memo’ do React (na primeira linha) e exportei o componente dentro da função ‘memo()’.
Agora, se voltarmos ao nosso painel de inspeção e fizermos a mesma análise (clicando no ‘reloding’, digitando no input e depois clicando na bolinha preenchida), veremos que o componente ‘PostItem’ só foi renderizado 100 vezes uma única vez: quando ele é mostrado em tela pela primeira vez. Nas outras vezes, ele não foi renderizado:
Perceba que agora ele apresenta uma cor cinza, indicando que não foi renderizado (desnecessariamente) a cada alteração do componente pai. Ou seja, aconteceu a renderização apenas do componente que teve seu estado alterado. Também podemos verificar isso ao passar o mouse por cima da cor cinza, onde aparecerá a mensagem ‘did not render’.
React.memo como facilitador para evitar renderizações desnecessárias
Evitar renderizações desnecessárias nem parece uma atividade tão complicada com a ajuda do React.memo, não é mesmo?
A melhor hora para utilização do React.memo é quando queremos abstrair parte do código e acabamos deixando esses componentes sendo renderizados (devido a alterações nos componentes pai que não interferem em nada nos componentes filhos).
Espero que essa dica possa te ajudar a evitar renderizações desnecessárias e conseguir melhores performances em suas aplicações. Qualquer dúvida, pode deixar nos comentários, combinado? Até mais!