This is a translation of the original post
One simple trick to optimize React re-renders by
Kent C. Dodds.
Eu estava preparando um post para o blog sobre um assunto relacionado a re-renderizações no React quando me deparei com essa pequena jóia de conhecimento do React que acredito que você vai gostar muito:
Após ler esse post no blog, Brooks Lybrand implementou esse truque e esse foi o resultado:
Animado? Vamos desvendar com um exemplo simples e fictício e então discutir sobre qual aplicação prática isso tem para você em seus aplicativos do dia a dia.
Um exemplo
// brinque com isso no codesandbox: https://codesandbox.io/s/react-codesandbox-g9mt5
import * as React from 'react'
import ReactDOM from 'react-dom'
function Logger(props) {
console.log(`${props.label} renderizado`)
return null // o que é retornado aqui é irrelevante...
}
function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<div>
<button onClick={increment}>O contador está em {count}</button>
<Logger label="contador" />
</div>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'))
Quando isso é executado, "contador renderizado" será registrado no console inicialmente e toda vez que o contador for incrementado, "contador renderizado" será registrado no console. Isso acontece porque quando o botão é clicado, o estado muda e o React precisa obter os novos elementos React para renderizar com base nessa mudança de estado. Quando ele obtém esses novos elementos, ele os renderiza e os aplica ao DOM.
Aqui está onde as coisas ficam interessantes. Considere o fato de que nunca muda entre as renderizações. É estático e, portanto, poderia ser extraído. Vamos tentar isso só por diversão (não estou recomendando que você faça isso, espere mais tarde na postagem do blog por recomendações práticas).
// brinque com isso no codesandbox: https://codesandbox.io/s/react-codesandbox-o9e9f
import * as React from 'react'
import ReactDOM from 'react-dom'
function Logger(props) {
console.log(`${props.label} renderizado`)
return null // o que é retornado aqui é irrelevante...
}
function Counter(props) {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<div>
<button onClick={increment}>O contador está em {count}</button>
{props.logger}
</div>
)
}
ReactDOM.render(
<Counter logger={<Logger label="contador" />} />,
document.getElementById('root'),
)
Você percebeu a mudança? Sim! Temos o registro inicial, mas então não recebemos novos registros quando clicamos no botão! O QUÊ!?
Se você quiser pular todos os detalhes técnicos profundos e ir direto para "o que
isso significa para mim", vá em frente e vá direto para lá agora
O que está acontecendo?
Então, o que está causando essa diferença? Bem, tem a ver com os elementos do React. Por que você não dá uma pausa rápida e lê minha postagem no blog "O que é JSX?" para ter um rápido lembrete sobre os elementos do React e sua relação com o JSX.
Quando o React chama a função do contador, ele recebe algo que se parece um pouco com isso:
// algumas coisas removidas para clareza
const elementoDoContador = {
type: 'div',
props: {
children: [
{
type: 'button',
props: {
onClick: increment, // este é o manipulador de clique
children: 'O contador está em 0',
},
},
{
type: Logger, // esta é a nossa função de componente de registro
props: {
label: 'contador',
},
},
],
},
}
Esses são chamados de objetos descritores de IU. Eles descrevem a IU que o React deve criar no DOM (ou via componentes nativos para react native). Vamos clicar no botão e dar uma olhada nas mudanças:
const elementoDoContador = {
type: 'div',
props: {
children: [
{
type: 'button',
props: {
onClick: increment,
children: 'O contador está em 1',
},
},
{
type: Logger,
props: {
label: 'contador',
},
},
],
},
}
Até onde podemos ver, as únicas mudanças são as props onClick e children do elemento button. No entanto, todo o conteúdo é completamente novo! Desde os primórdios do uso do React, você está criando esses objetos totalmente novos em cada renderização. (Felizmente, até os navegadores móveis são bastante rápidos nisso, então isso nunca foi um problema de desempenho significativo).
Pode ser mais fácil investigar as partes deste conjunto de elementos do React que são iguais entre as renderizações, então aqui estão as coisas que NÃO mudaram entre essas duas renderizações:
const elementoDoContador = {
type: 'div',
props: {
children: [
{
type: 'button',
props: {
onClick: increment,
children: 'O contador está em 1',
},
},
{
type: Logger,
props: {
label: 'contador',
},
},
],
},
}
Todos os tipos de elementos são iguais (isso é típico), e a propriedade label para o elemento Logger não mudou. No entanto, o objeto de props em si muda a cada renderização, mesmo que as propriedades desse objeto sejam iguais ao objeto de props anterior.
Ok, aqui está o ponto crucial. Porque o objeto de props do Logger mudou, o React precisa reexecutar a função do Logger para garantir que não receba nenhum novo JSX com base no novo objeto de props (além dos efeitos que podem precisar ser executados com base na mudança de props). Mas e se pudéssemos evitar que as props mudassem entre as renderizações? Se as props não mudarem, o React sabe que nossos efeitos não precisam ser reexecutados e nosso JSX não deve mudar (porque o React depende do fato de que nossos métodos de renderização devem ser idempotentes). É exatamente isso que o React é codificado para fazer bem aqui e tem sido assim desde o início do React!
Ok, mas o problema é que o React cria um novo objeto de props toda vez que criamos um elemento do React, então como garantir que o objeto de props não mude entre as renderizações? Espero que agora você entenda por que o segundo exemplo acima não estava re-renderizando o Logger. Se criarmos o elemento JSX uma vez e reutilizarmos o mesmo, obteremos o mesmo JSX todas as vezes!
Vamos juntar tudo novamente
Aqui está o segundo exemplo novamente (para que você não precise rolar para cima):
// brinque com isso no codesandbox: https://codesandbox.io/s/react-codesandbox-o9e9f
import * as React from 'react'
import ReactDOM from 'react-dom'
function Logger(props) {
console.log(`${props.label} renderizado`)
return null // o que é retornado aqui é irrelevante...
}
function Counter(props) {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<div>
<button onClick={increment}>O contador está em {count}</button>
{props.logger}
</div>
)
}
ReactDOM.render(
<Counter logger={<Logger label="counter" />} />,
document.getElementById('root'),
)
Então vamos verificar as coisas que são iguais entre as renderizações:
const counterElement = {
type: 'div',
props: {
children: [
{
type: 'button',
props: {
onClick: increment,
children: 'O contador está em 1',
},
},
{
type: Logger,
props: {
label: 'counter',
},
},
],
},
}
Como o elemento Logger está completamente inalterado (e, portanto, as props também estão inalteradas), o React pode automaticamente fornecer essa otimização para nós e não se preocupar em re-renderizar o elemento Logger, pois ele não deveria precisar ser re-renderizado de qualquer maneira. Isso é basicamente como o React.memo, exceto que, em vez de verificar cada uma das props individualmente, o React verifica o objeto de props holisticamente.
Então, o que isso significa para mim?
Em resumo, se você estiver enfrentando problemas de desempenho, tente isso:
1 - "Eleve" o componente caro para um pai onde ele será renderizado com menos frequência.
2 - Em seguida, passe o componente caro como uma propriedade.
Você pode descobrir que fazer isso resolve seu problema de desempenho sem precisar espalhar o React.memo por todo o seu código como um grande band-aid intrusivo 🤕😉
Demo
Criar um exemplo prático de um aplicativo lento em React é complicado porque geralmente requer a construção de um aplicativo completo, mas eu tenho um exemplo de aplicativo criado artificialmente que tem uma versão antes/depois para você conferir e brincar aqui
⚠️
Uma coisa que eu quero adicionar é que, mesmo que seja melhor usar a versão mais rápida deste código, ele ainda tem um desempenho muito ruim quando é renderizado inicialmente e teria um desempenho muito ruim se precisasse realmente fazer outra renderização completa (ou quando você atualiza as linhas/colunas). Isso é um problema de desempenho que provavelmente deveria ser tratado por seus próprios méritos (independente de quão necessárias são as re-renderizações). Além disso, por favor, lembre-se de que o CodeSandbox usa a versão de desenvolvimento do React, que oferece uma experiência de desenvolvimento muito boa, mas tem um desempenho MUITO mais lento do que a versão de produção do React.
⚠️
E isso não é apenas algo útil no nível superior do seu aplicativo. Isso pode ser aplicado em qualquer lugar do seu aplicativo onde faça sentido. O que eu gosto nisso é que "É natural para composição e atua como uma oportunidade de otimização." (foi o Dan quem disse). Então, faço isso naturalmente e obtenho os benefícios de desempenho de graça. E é isso que sempre amei no React. O React é escrito de forma que aplicativos React idiomáticos sejam rápidos por padrão, e então o React fornece ajudantes de otimização para você usar como saídas de emergência.
Boa sorte!
Top comments (0)