Skip to content
This page has been auto-translated and may contain errors.View in English

Como React renderiza

"Renderizar" é uma das palavras mais usadas em React e uma das menos explicadas. Entender o que realmente acontece entre uma atualização de estado e os pixels mudando na tela esclarece bastante coisa: por que componentes executam quando executam, por que a maioria das re-renderizações não custa praticamente nada, e onde de verdade o trabalho de performance deveria começar. Este capítulo constrói esse modelo mental. Os dois capítulos que vêm depois, Memoização e Code splitting, colocam isso em prática.

Uma renderização é uma chamada de função

Quando React "renderiza" um componente, ele chama sua função de componente:

jsx
function Counter() {
  const [count, setCount] = useState(0)
  console.log('Counter renderizou') // executa toda vez que React chama essa função

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

É só isso. A função executa de cima a baixo, qualquer cálculo dentro dela executa de novo, e ela retorna JSX descrevendo como a interface deveria parecer agora. Nada disso envolve o DOM do navegador ainda. Uma renderização produz uma descrição; ela não toca na página.

Essa distinção importa porque essas duas coisas têm custos bem diferentes. Chamar uma função JavaScript e construir uma descrição é rápido. Mudar o DOM de verdade, o que dispara trabalho de layout e pintura no navegador, é a parte cara. Todo o design do React se apoia em fazer a coisa barata livremente e a coisa cara o mínimo possível.

Os três passos

Uma única "re-renderização" passa por três passos. O gatilho geralmente é uma atualização de estado, mas a montagem inicial, um componente pai renderizando acima do componente, e um novo valor de contexto também executam a mesma maquinaria.

  1. Renderizar. React chama a função do componente cujo estado mudou, e então, recursivamente, todos os componentes que ela retorna. O resultado é um novo virtual DOM: uma descrição leve em memória daquela parte da interface.
  2. Reconciliação. React executa um algoritmo de diff comparando o novo virtual DOM contra o anterior, descobrindo exatamente o que mudou entre eles.
  3. Commit. As diferenças, e apenas as diferenças, são aplicadas ao DOM de verdade. Essa é a parte que o usuário consegue ver.

O curso oferece uma analogia que vale manter. Um arquiteto atualizando um prédio primeiro redesenha a planta (renderizar), depois lista quais mudanças a nova planta realmente exige (reconciliação), e só aí o time de construção toca no prédio (commit). Redesenhar uma planta é rápido. Construção é onde tempo e dinheiro vão, então o time faz o mínimo de trabalho que a lista permite.

Renderizar e reconciliação permanecem rápidos, e o commit fica limitado ao que o diff encontrou, é por isso que React se sente rápido por padrão. Quando algo fica lento, o Profiler te diz qual passo culpar em vez de deixar você adivinhar.

Todo clique no botão Counter executa os três: o log dispara, React faz o diff do resultado, e o commit atualiza o nó de texto do botão e mais nada ao seu redor.

Renderizar é recursivo, e isso normalmente é ok

Quando um componente renderiza, React renderiza tudo que ele retorna, depois tudo que esses componentes retornam, descendo cada branch até o fim antes de passar para o próximo. Uma mudança de estado em um componente pai portanto re-renderiza todos os seus descendentes por padrão, independente de receberem ou não o estado mudado como props.

Isso soa como desperdício até você separar os passos de novo. Essas renderizações filhas são chamadas de função produzindo descrições. Durante a reconciliação, React não encontra atributos mudados em nenhuma delas, então o commit não tem trabalho de DOM a fazer para aquela subárvore. O trabalho visível de DOM do usuário fica limitado ao que realmente mudou. Re-renderizar uma subárvore que não faz commit de nada é o caso normal e saudável, e para a imensa maioria dos componentes é muito barato demais para medir.

Para de ser ok em duas situações: quando um componente faz trabalho caro durante sua renderização, ou quando uma subárvore é tão grande que até renderizações baratas se acumulam. Esses são exatamente os casos que Memoização aborda. Resista ao impulso de recorrer a ela antes de ter evidência, o que nos traz a medição.

Meça antes de otimizar

React já é rápido, e a reconciliação já mantém atualizações de DOM mínimas. O conselho do curso é direto: seja capaz de medir um problema de performance antes de aplicar uma técnica para resolvê-lo, e pese toda otimização contra a legibilidade do código que ela complica. Astúcia que ninguém consegue manter também é um custo.

Duas ferramentas cobrem a maioria das necessidades de medição:

  • A aba Performance do navegador. Registre uma sessão, execute a interação lenta, pare, e inspecione o que executou e quanto tempo levou. Seu throttling de CPU (tente uma desaceleração 4x ou 6x) e throttling de rede deixam sua máquina rápida de desenvolvimento simular os dispositivos e conexões mais lentos que seus usuários realmente têm. Problemas invisíveis no seu laptop aparecem imediatamente.
  • O React DevTools Profiler. Uma extensão de navegador do time React, vivendo em sua própria aba Profiler. O mesmo fluxo de registrar-agir-parar, mas a saída tem formato React: quais componentes renderizaram, por que renderizaram, quanto tempo cada um levou, e quanto tempo o commit levou. Para diagnosticar apps React especificamente, geralmente é a ferramenta mais direta.

StrictMode: um barreira para desenvolvimento

StrictMode é um componente que você envolve ao redor do seu app (ou qualquer subárvore) para ativar verificações apenas para desenvolvimento. É o wrapper que Setup coloca ao redor de <App /> em main.jsx.

Em desenvolvimento, StrictMode deliberadamente chama duas vezes suas funções de componente, junto com as outras funções que React espera serem puras: inicializadores de useState e useReducer, os updaters que você passa a um setter, reducers, e cálculos de useMemo. Executa cada effect uma vez extra, configurando, limpando, e configurando de novo, e reatribui callbacks de ref. Também avisa sobre APIs descontinuadas. Em um build de produção não faz nada, então nunca desacelera ou re-renderiza o app que seus usuários veem.

Executar duas vezes soa como sabotagem até você ver o que pega. Uma função de componente deveria ser pura: mesmas entradas, mesma saída, sem efeitos colaterais durante renderização. Se renderizar duas vezes produz um resultado diferente de renderizar uma vez, o componente estava escondendo um bug.

O curso demonstra isso com um componente que chama push em um array definido fora dele durante renderização; a página fica bem até qualquer mudança de estado re-executar a função e fazer push de um duplicado. Sob StrictMode o duplicado aparece na primeira carga, e o fix (copiar o array antes de modificá-lo, ou mover o trabalho para fora da renderização) segue disso.

Pureza cobre seus updaters

O mesmo requisito de pureza cobre o updater em setCount(c => c + 1), já que essa função também executa duas vezes.

O teste de re-execução do effect testa o mesmo contrato do outro lado. Effects é dono da regra de limpeza e mostra como fica sem uma sob StrictMode, quando um setInterval não limpo deixa você contando de dois em dois e vazando um timer a cada montagem.

Ver mais bugs em desenvolvimento parece estranho de início, mas é exatamente o objetivo. Todo bug que StrictMode expõe já existia e de outra forma teria esperado a produção para se apresentar.

Nota de versão

O comportamento do effect chegou no React 18: ao montar, StrictMode executa o setup de cada effect, depois sua limpeza, depois setup de novo. Artigos mais antigos descrevem StrictMode como apenas re-renderizando componentes. O curso escreve o wrapper como React.StrictMode; com named imports é <StrictMode>.

O passo de diff é mais barato do que "comparar duas árvores" sugere porque React recusa fazer uma comparação completa de árvore, o que seria O(n³) no caso geral. As heurísticas: elementos de tipos diferentes derrubam a subárvore antiga inteira e reconstruem, elementos do mesmo tipo são mantidos e apenas seus atributos mudados são remendados, e a reconciliação de lista é guiada por keys, é por isso que keys devem ser identidades estáveis.

React recorre a Object.is nas comparações que controlam trabalho: não atualizar quando você seta estado para o mesmo valor, verificar arrays de dependência, e a comparação rasa de props do memo. Dois objetos profundamente iguais com identidades diferentes falham nos três. Esse é o fio que corre pelo próximo capítulo: um pai passando {} ou uma função arrow inline cria uma identidade fresca a cada renderização, o que é inofensivo por padrão mas derrota toda otimização baseada em identidade no momento em que você adiciona uma.

Os próprios docs do React agrupam os dois primeiros passos em uma única fase de render: o diff acontece enquanto React caminha pela árvore elemento por elemento, então nada espera por uma descrição completa ser construída primeiro. Um bailout pode portanto parar no meio de uma branch. Separar reconciliação é uma conveniência de ensino para descrever o que o diff faz, e o modelo de duas fases render/commit é o que o Profiler e o código fonte do React usam.

Essa fase de render também, por design, é permitida ser descartada. React pode começar renderizando, descartar o trabalho, e renderizar de novo antes de fazer commit, e features concorrentes se apoiam nisso para manter a main thread responsiva.

Essa é a razão mais profunda pela qual renders devem ser puras: React só garante que trabalho commitado acontece exatamente uma vez. Qualquer coisa que um componente faz durante a fase de render (mutações, subscriptions, logging em que você se baseia) pode acontecer zero, uma, ou várias vezes. O double-invoke do StrictMode é uma simulação barata dessa realidade em desenvolvimento, e código que quebra sob isso é código que renderização concorrente pode quebrar de verdade.

Quando o Profiler realmente mostra um problema de verdade, combine a ferramenta ao passo. Barras de render longas em um componente fazendo computação pesada apontam para useMemo; uma subárvore larga re-renderizando com nada para fazer commit aponta para memo; as duas vivem em Memoização. Uma carga inicial lenta, onde o problema é JavaScript chegando antes de qualquer renderização conseguir começar, é um problema de bundle em vez de um problema de render, e Code splitting é a alavanca.

Às vezes o fix é estrutural: mover estado para baixo no componente que o usa, ou passar subárvores caras como children para que o re-render do pai stateful veja os mesmos objetos elemento e faça bailout daquela branch, remove renderizações sem nenhuma memoização.

JunoRenders são baratos, commits são precisos Um render significa React chama sua função de componente de novo para perguntar como a tela deveria parecer. Ele compara a nova resposta com a antiga e atualiza só as partes da página que realmente mudaram. Quando um pai renderiza, todos os seus filhos renderizam também, e isso é normal e quase sempre rápido.

Envolva seu app em StrictMode enquanto você constrói: ele executa coisas duas vezes de propósito em desenvolvimento para ajudar você a encontrar bugs cedo, e se desativa no app finalizado.

JunoRenders são baratos, commits são precisos Três passos: render chama suas funções de componente e constrói um novo virtual DOM, reconciliação faz o diff contra o antigo, commit aplica só as diferenças ao DOM de verdade. Renders de pai em cascata descem para filhos por padrão, e já que a maioria desses renders não faz commit de nada, eles custam quase nada.

Antes de otimizar qualquer coisa, registre a interação lenta no React DevTools Profiler e faça throttle da sua CPU para ver o que usuários veem.

Mantenha StrictMode ligado: renders double-invoked expõem componentes impuros, e re-executar effects expõe limpeza faltante como um setInterval não limpo.

JunoRenders são baratos, commits são precisos Reconciliação é heurística: mudanças de tipo reconstroem subárvores, elementos do mesmo tipo fazem patch no lugar, keys guiam o diff de lista, e as comparações que controlam trabalho (state bailout, arrays de dependência, memo) executam em Object.is, então object e function identidades frescas se leem como mudanças.

Trabalho da fase render é descartável por contrato, é por isso que pureza importa e por isso StrictMode simula a dupla execução que renderização concorrente pode produzir.

Profile primeiro, depois combine o fix ao passo: memoize renders caros, divida bundles para cargas lentas, ou reestruture com children para que o trabalho nunca aconteça mesmo.

Próximo: Memoização, onde igualdade referencial decide o que React consegue pular.