Guia de React re-render: tudo, tudo de uma vez

dougsource

doug-source

Posted on May 31, 2024

Guia de React re-render: tudo, tudo de uma vez

NOTA: Este artigo é apenas uma tradução. A fonte dele está no rodapé.

Guia completo sobre re-renders do React. O guia explica o que são re-renders, o que é um re-render necessário e desnecessário, o que pode acionar um re-render de React component.

Também inclui os patterns mais importantes que podem ajudar a evitar re-renders e alguns anti-patterns que levam a re-renders desnecessários e, como resultado, baixo desempenho. Cada pattern e anti-pattern é acompanhado por um auxílio visual e um exemplo de código funcional.

cat reading

O que é o re-render no React?

Ao falar sobre o desempenho do React, há dois estágios principais com os quais precisamos nos preocupar:

  • initial render: acontece quando um component aparece pela primeira vez na tela
  • re-render: segunda e qualquer renderização consecutiva de um componente que já está na tela

Re-render acontece quando o React precisa atualizar o app com alguns dados novos. Geralmente, isso acontece como resultado da interação de um usuário com o app ou de alguns dados externos provenientes de uma request assíncrona ou de algum modelo de subscription.

Apps não interativos que não possuem atualizações de dados assíncronas nunca serão re-render e, portanto, não precisam se preocupar com a otimização do desempenho das novas renderizações.

Assista "Intro to re-renders" no YouTube

🧐 O que é um re-render necessário e o que é um re-render desnecessário?

Re-render necessário: re-render de um component que é a origem das alterações ou de um component que usa diretamente as novas informações. Por exemplo, se um usuário digitar um input field, o component que gerencia seu estado precisa se atualizar a cada pressionamento de tecla, ou seja, re-renderizar.

Re-render desnecessário: re-render de um component que é propagado pelo app por meio de diferentes mecanismos de re-render devido a erro ou arquitetura ineficiente do app. Por exemplo, se um usuário digitar um input field e a página inteira for re-render a cada pressionamento de tecla, a página terá sido re-render desnecessariamente.

Re-renders desnecessários por si só não são um problema: React é muito rápido e geralmente capaz de lidar com elas sem que os usuários percebam nada.

No entanto, se os re-renders acontecerem com muita frequência e/ou em components muito pesados, isso poderá fazer com que a experiência do usuário pareça com "lag", delays visíveis em cada interação ou até mesmo o app pare completamente de responder.

Assista "Intro to re-renders" no YouTube

Quando um React component re-render novamente?

Existem quatro razões pelas quais um component re-render: mudanças de state, re-renders de parents (ou childrens), mudanças de context e mudanças de hooks. Há também um grande mito: que as re-renders acontecem quando as props do component mudam. Por si só, não é verdade (veja a explicação abaixo).

🧐 Razões para re-renders: mudanças de state

Quando um state do component muda, ele irá re-render novamente. Geralmente, isso acontece ou em um callback ou no useEffect hook.

As mudanças de state são a fonte "raiz" de todos os re-renders.

código de exemplo

🧐 Razões para re-renders: re-render de parents

Um component será re-render se seu parent também re-render. Ou, se olharmos para isso na direção oposta: quando um component é re-render, ele também re-render todos os seus children.

Ele sempre "desce" na árvore: o re-render de um child não aciona o re-render de um parent. (Existem algumas advertências e casos extremos aqui, consulte o guia completo para mais detalhes: O mistério do React Element, children, parents e re-renders).

código de exemplo

🧐 Razões para re-renders: mudanças de context

Quando o valor no Context Provider for alterado, todos os components que usam esse Context serão re-render, mesmo que não usem a parte alterada dos dados diretamente. Esses re-renders não podem ser evitados diretamente com a memoization, mas existem algumas workarounds que podem simular isso.

código de exemplo

🧐 Razões para re-renders: mudanças nos hooks

Tudo o que está acontecendo dentro de um hook "pertence" ao component que o utiliza. As mesmas regras relativas às mudanças de context e state se aplicam aqui:

  • a mudança de state dentro do hook irá desencadear uma re-rerender inevitável do component "host"
  • se o hook usar o Context e as alterações de valor do Context, ele acionará uma re-rerender inevitável do component "host"

Hooks podem ser encadeados. Cada hook único dentro da cadeia ainda "pertence" ao "host" component, e as mesmas regras se aplicam a qualquer um deles.

código de exemplo

⛔️ Razões para re-renders: mudanças de props (o grande mito)

Não importa se as props do component mudam, ou não, quando se fala em re-renders de components não-memoized.

Para que as props sejam alteradas, elas precisam ser atualizadas pelo parent component. Isso significa que o parent teria que re-render, o que acionaria o re-render do child component, independentemente de suas props.

Somente quando técnicas de memoization são usadas (React.memo, useMemo), a mudança de props se torna importante.

código de exemplo

Evitando re-renders com composition

⛔️ Antipattern: Criando components na render function

Criar components dentro da render function de outro component é um anti-pattern que pode ser o maior destruidor de desempenho. Em cada re-render, o React irá re-mount este component (ou seja, destruí-lo e recriá-lo do zero), o que será muito mais lento do que um re-render normal. Além disso, isso levará a bugs como:

  • possíveis "flashes" de conteúdo durante os re-renders
  • state sendo redefinido no componente a cada re-render
  • useEffect sem dependências disparadas em cada re-render
  • se um component estiver focused, o focus será perdido

Mais recursos:

✅ Evitando re-renders com composition: movendo o state "para baixo"

Esse pattern pode ser benéfico quando um component pesado gerencia o state, e esse state é usado apenas em uma pequena parte isolada da render tree. Um exemplo típico seria abrir/fechar um dialog com um clique de button em um component complicado que renderiza uma parte significativa de uma página.

Nesse caso, o state que controla a aparência do modal dialog, o próprio dialog e o button que aciona a atualização podem ser encapsulados em um component menor. Como resultado, o component maior não será re-render nessas mudanças de state.

código de exemplo

✅ Evitando re-renders com composition: children como props

Isso também pode ser chamado de "wrap state around children". Esse pattern é semelhante a "moving state down": ele encapsula mudanças de state em um component menor. A diferença aqui é que o state é usado em um elemento que encapsula uma parte lenta da render tree, portanto não pode ser extraído tão facilmente. Um exemplo típico seria callbacks de onScroll ou onMouseMove attached ao root element de um component.

Nessa situação, o state management e os components que usam esse state podem ser extraídos para um component menor, e o component lento pode ser passado para ele como children. Da perspectiva dos components menores, children são apenas props, portanto, não serão afetados pela mudança de state e, portanto, não serão re-render.

código de exemplo

✅ Evitando re-renders com composition: components como props

Praticamente igual ao pattern anterior, com o mesmo comportamento: ele encapsula o state dentro de um component menor e components pesados são passados para ele como props. As props não são afetadas pela mudança de state, portanto, components pesados não serão re-render.

Pode ser útil quando alguns components pesados são independentes do state, mas não podem ser extraídos como children como um grupo.

código de exemplo

Evitando re-renders com React.memo

Encapsulando um component em React.memo interromperá a cadeia downstream de re-renders que é acionada em algum lugar acima da render tree, a menos que as props deste component tenham mudado.

Isso pode ser útil ao renderizar um component pesado que não depende da origem do re-render (ou seja, state, dados alterados).

✅ React.memo: component com props

Todas as props que não são valores primitivos devem ser memoized para que React.memo funcione

código de exemplo

✅ React.memo: components como props ou children

React.memo deve ser aplicado aos elements passados como children/props. Memoize o parent component não funcionará: children e props serão objetos, portanto, eles mudarão a cada re-render.

código de exemplo

Melhorando o desempenho de re-renders com useMemo/useCallback

⛔️ Antipattern: useMemo/useCallback desnecessários em props

Memoizing props por sí só não impedirá re-renders de um child component. Se um parent component re-renders, ele acionará o re-render de um child component independentemente de suas props.

código de exemplo

✅ useMemo/useCallback necessário

Se um child component é encapsulado em React.memo, todas as props que não são valores primitivos deverão ser memoized

código de exemplo

Se um component usa valor não-primitivo como dependência em hooks como useEffect, useMemo, useCallback, ele deve ser memoized.

Veja um exemplo no codesandbox

código de exemplo

✅ useMemo para cálculos pesados

Um dos casos de uso useMemo é evitar cálculos caros em cada re-render.

useMemo tem seu custo (consome um pouco de memória e torna o render inicial um pouco mais lento), portanto não deve ser usado em todos os cálculos. No React, mounting e updating components será o cálculo mais caro na maioria dos casos (a menos que você esteja realmente calculando números primos, o que você não deveria fazer no frontend de qualquer maneira).

Como resultado, o caso de uso típico useMemo seria memoize React elements. Geralmente partes de uma render tree existente ou resultados de uma render tree gerada, como uma map function que retorna novos elements.

O custo de "pure" javascript operations, como ordenar ou filtering um array, geralmente é insignificante, em comparação com atualizações de components.

código de exemplo

Melhorando o desempenho de re-render de listas

Além das regras regulares e patterns de re-render, o key attribute pode afetar o desempenho das listas no React.

Importante: apenas fornecer key attributes não melhorará o desempenho das listas.

Para evitar rerenders de list elements, você precisa encapsular eles com React.memo e seguir todas as práticas recomendadas.

O valor em key deve ser uma string consistente entre os re-renders para cada elemento da lista. Normalmente, o id do item ou o index de arrays são usados para isso.

Não há problema em usar index de arrays como key, se a lista for estática , ou seja, os elementos não são adicionados/removidos/inseridos/reordenados.

Usar o index do array em listas dinâmicas pode levar a:

  • bugs se os itens tiverem estado ou quaisquer uncontrolled elements (como form inputs)
  • desempenho degradado se os itens forem agrupados em React.memo

Recursos adicionais:

código de exemplo

⛔️ Antipattern: valor randômico como key em listas

Valores gerados randomicamente nunca deveria ser usados como valores no key attribute em listas. Elas levarão ao re-mounting de itens do React em cada nova renderização, o que levará a:

  • desempenho muito ruim da lista
  • bugs se os itens tiverem state ou quaisquer uncontrolled elements (como form inputs)

Veja um exemplo no codesandbox

código de exemplo

Evitando re-renders causados pelo Context

✅ Evitando Context re-renders: memoizing o valor do Provider

Se o Context Provider não for colocado na raiz do app e houver a possibilidade de ele re-render devido a alterações em seus ancestrais, seu valor deverá ser memoized.

Veja um exemplo no codesandbox

código de exemplo

✅ Evitando Context re-renders: splitting de dados e API

Se no contexto houver uma combinação de dados e API (getters e setters), eles poderão ser "split" (divididos) em diferentes providers no mesmo component. Dessa forma, os components que usam apenas API não serão re-render quando os dados forem alterados.

Leia mais sobre esse pattern aqui: Como escrever React apps de alto desempenho com Context

Veja um exemplo no codesandbox

código de exemplo

✅ Evitando Context re-renders: splitting dados em chunks

Se o Context gerenciar alguns chunks de dados independentes, eles poderão ser divididos em providers menores no mesmo provider. Dessa forma, apenas os consumers do chunk alterado serão re-render.

Leia mais sobre esse pattern aqui: Como escrever React apps de alto desempenho com Context

Veja um exemplo no codesandbox

código de exemplo

✅ Evitando Context re-renders: Context selectors

Não há como evitar que um component, que usa uma parte do valor do Context, seja re-render, mesmo que o dado usado não tenha sido alterado, mesmo com o useMemo hook.

Context selectors, no entanto, podem ser "faked" com o uso de higher-order components e React.memo.

Leia mais sobre esse pattern aqui: Higher-Order Components na era dos React Hooks

Veja um exemplo no codesandbox

código de exemplo

Fonte

React re-renders guide: everything, all at once Makarevich - por Nadia Makarevich

💖 💪 🙅 🚩
dougsource
doug-source

Posted on May 31, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related