---
title: "A Otimização Mais Subestimada do React: Organização da Árvore de Componentes"
summary: "Dois padrões estruturais que eliminam re-renders desnecessários no React sem React.memo — mais de 4.000% nos benchmarks — e o que o React Compiler ainda deixa por sua conta."
published: 2026-08-08
tags: [react, performance, architecture]
lang: pt-BR
canonical: https://victorleao.dev/pt-BR/articles/the-most-underrated-react-optimization
---

O React faz exatamente o que você projeta para ele fazer. Quando uma aplicação parece lenta, o instinto é culpar o framework. Na maioria das vezes, o framework está bem. A árvore que ele recebeu não está.

![Before memo, design the tree.](https://victorleao.dev/images/articles/react-tree-optimization/cover.png)

A árvore de componentes que você escreve determina quais componentes re-renderizam, com que frequência e a que custo. Onde o estado mora, quem renderiza o quê, como os componentes são compostos: são decisões estruturais com consequências diretas de performance. O React as segue fielmente, deliberadas ou não.

Na maioria das vezes, não são deliberadas. Elas se acumulam. Uma funcionalidade precisa de um dado, então o estado sobe. Outra funcionalidade precisa do mesmo dado, então ele fica ali. Um wrapper de layout cresce. Um Provider é colocado na raiz "por garantia". Seis meses depois, um `useState` que pertence dois níveis abaixo mora num pai que re-renderiza toda a sua subárvore a cada interação. Ninguém o colocou ali de propósito. Ninguém o moveu quando os sintomas apareceram.

Isso não é o React se comportando mal. Isso é o React fazendo exatamente o que foi mandado.

Ao final deste artigo, você vai:

- entender por que a estrutura da árvore — não a memoização — é a alavanca principal para controlar re-renders
- reconhecer dois padrões que eliminam renders desnecessários sem um único wrapper
- ver o que os dados de benchmark mostram sobre o custo real de errar isso

Nenhuma API nova. A otimização é inteiramente arquitetural.

## Re-renderizar não é o inimigo

Aqui está algo que a maioria dos tutoriais de React inverte: re-renderizar não é o problema.

Re-renderizar é como o React funciona. Quando o estado muda, o React chama a função do componente, gera uma nova descrição de UI, compara com a anterior, e atualiza o DOM onde algo difere. O ciclo é rápido, leve e exatamente correto. Re-renderizar não é um modo de falha — é o mecanismo que mantém sua UI sincronizada com seu estado. Uma aplicação React que não re-renderiza é uma aplicação quebrada.

O problema é o **re-render desnecessário**: componentes sendo chamados cuja saída não vai mudar, porque nada do que eles dependem mudou, porque o único motivo de terem rodado é que o pai deles rodou primeiro.

Isso acontece porque renderizar é recursivo. Quando um componente re-renderiza, o React chama seus filhos. E os filhos deles. E os filhos dos filhos. Não porque esses componentes tenham qualquer relação com o estado que mudou — mas porque estão downstream do componente que tem. O React não analisa a árvore para determinar quem de fato depende do quê. Ele propaga para baixo e confia no seu algoritmo de diffing para detectar se algo precisa atualizar.

Existem exatamente duas coisas que fazem um componente re-renderizar: o próprio estado dele muda, ou o pai dele re-renderiza. Essa é a lista completa.

Agora, por que o re-render desnecessário prejudica? Um re-render envolve duas fases distintas. A **Render Phase** chama as funções dos componentes, constrói o novo virtual DOM, e roda o algoritmo de diffing para identificar o que mudou — mais barato do que o que vem a seguir, mas não é grátis. A **Commit Phase** aplica essas mudanças identificadas ao DOM real. Operações de DOM estão entre as coisas mais custosas que um navegador executa: recálculo de layout, pintura, composição. O algoritmo de diffing O(n) do React existe justamente para minimizá-las.

O risco dos re-renders desnecessários não é só as chamadas de função extras. É o que essas chamadas podem disparar. Um componente que re-renderiza quando não deveria pode produzir uma saída que diverge do que o React tinha em cache — novas referências de objeto passadas como props para os filhos, valores derivados recalculados, nomes de classe ligeiramente diferentes. O diffing do React encontra essas diferenças e as aplica ao DOM real. Mutações que não teriam acontecido se o componente nunca tivesse sido chamado. O re-render desnecessário foi a condição. A mutação do DOM foi o custo.

O `React.memo` resolve isso adicionando uma comparação de props antes da chamada da função: se as props não mudaram, pula o render. É uma ferramenta válida. Mas ela compensa o fato do pai re-renderizar — não muda de onde a propagação começa. Antes de recorrer ao `memo`, pergunte: por que esse pai re-renderiza no contexto deste componente? Ele precisa?

O React Compiler responde essa pergunta com uma máquina. Ele insere a memoização para você, em cada nível e com mais precisão do que você faria à mão — bom o suficiente pra que os benchmarks deste artigo praticamente se fechem uma vez que ele está habilitado. Onde ele está habilitado, e onde seus componentes seguem as Rules of React o bastante pra compilar: os que não seguem são pulados, silenciosamente.

Ainda assim é compensação. O compilador faz cache em volta da árvore que você escreveu; ele não consegue decidir que um `useState` pertence dois níveis abaixo, ou que um Provider está alimentando cinquenta componentes que nunca o leem. Essas são as decisões sobre as quais este artigo trata, e nenhum compilador as toma por você.

## Onde a propagação deveria parar

Todo re-render desnecessário é uma propagação que viajou mais longe do que o estado exigia. A pergunta estrutural é sempre a mesma: onde esse re-render deveria ter parado?

Dois padrões respondem essa pergunta de ângulos diferentes. Nenhum usa memoização. A otimização vem de redesenhar onde a fronteira fica — antes do render, não interceptando-o depois.

### Movendo a origem

O caso mais comum: o estado mora mais alto do que o componente que o usa, e componentes sem relação estão downstream.

```jsx
// ❌ count mora em Parent — os irmãos caros re-renderizam a cada clique
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveComponent />
      <AnotherExpensiveComponent />
    </div>
  );
}
```

`Parent` guarda o estado. Todo clique o re-renderiza — o que re-renderiza `ExpensiveComponent` e `AnotherExpensiveComponent`. Nenhum dos dois tem qualquer relação com `count`. Eles não o leem. Eles não renderizam nada que dependa dele. Eles rodam porque estão downstream de um componente que mudou. Essa é a falha estrutural.

Mover o estado para baixo para a propagação no lugar certo.

```jsx
// ✅ count mora em Counter — os irmãos caros ficam intocados
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}

function Parent() {
  return (
    <div>
      <Counter />
      <ExpensiveComponent />
      <AnotherExpensiveComponent />
    </div>
  );
}
```

`Parent` não tem estado. Ele nunca re-renderiza por conta própria. Quando o usuário clica, `Counter` re-renderiza — e a propagação para ali, porque `ExpensiveComponent` e `AnotherExpensiveComponent` são irmãos de `Counter`, não filhos dele. O formato da árvore é quem faz o trabalho.

![Benchmark comparando o estado mantido no pai versus movido para Counter, ao longo de 100 cliques](https://victorleao.dev/images/articles/react-tree-optimization/state-colocation-benchmark.png)

*Após 100 cliques: o estado em Parent tem média de 3.53ms por render (352.90ms no total). Movido para Counter, 0.08ms — 4.367% mais rápido — porque Parent renderiza uma única vez, na montagem, e nunca mais.*

> **State colocation**
>
> O estado mora no componente mais próximo que de fato precisa dele. Quando o estado fica mais alto do que o necessário, a subárvore abaixo vira dano colateral a cada atualização. A correção não é um wrapper. É uma mudança de lugar.

> **O princípio**
>
> Se mover o estado para baixo não quebra nada, ele deveria estar lá embaixo.

### Bloqueando o caminho

Algum estado genuinamente não pode ser movido para baixo. O componente com estado é estrutural — ele precisa envolver o conteúdo caro como parte do layout. Mover o estado pra fora quebraria o propósito do componente.

O caso clássico: um input que filtra uma lista, onde os dois moram dentro do mesmo wrapper.

```jsx
// ❌ SearchLayout dono do estado — ExpensiveList re-renderiza a cada tecla
function SearchLayout() {
  const [query, setQuery] = useState('');

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ExpensiveList />
    </div>
  );
}
```

Cada tecla digitada re-renderiza `SearchLayout`. `ExpensiveList` é filho dele — re-renderiza junto, a cada tecla, sem nenhuma mudança na sua saída.

![Benchmark mostrando ExpensiveList re-renderizando a cada tecla porque é filho do SearchLayout, que tem estado](https://victorleao.dev/images/articles/react-tree-optimization/children-as-props-bad.png)

*Após 100 teclas digitadas: média de 3.54ms, 353.60ms no total. ExpensiveList renderiza 100 vezes.*

`ExpensiveList` renderiza 100 vezes. Não dá pra colocar o estado junto aqui. A maioria dos desenvolvedores instala `React.memo` em `ExpensiveList` e segue em frente. Mas a estrutura pode ser invertida.

```jsx
// ✅ StatefulInput recebe o conteúdo caro como children — sua referência fica estável
function StatefulInput({ children }) {
  const [query, setQuery] = useState('');
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {children}
    </div>
  );
}

function SearchLayout() {
  return (
    <StatefulInput>
      <ExpensiveList />
    </StatefulInput>
  );
}
```

`StatefulInput` é dono do estado — mas `ExpensiveList` é criado por `SearchLayout` e passado como `children`. Quando o usuário digita e `StatefulInput` re-renderiza, ele renderiza `{children}`. Esse `children` é o objeto de elemento React que `SearchLayout` produziu. `SearchLayout` não tem estado, não re-renderizou, e o elemento que ele passou carrega a mesma referência que carregava antes.

O React vê a mesma referência de elemento na mesma posição. Ele pula a chamada da função de `ExpensiveList`.

> Esse bailout — o React pulando o render de um componente quando a referência do seu elemento não mudou — está documentado por Dan Abramov em [Before You memo()](https://overreacted.io/before-you-memo/). Não é o mesmo mecanismo da comparação de props do `React.memo`. É uma otimização em nível de reconciliação: mesma referência na mesma posição significa nenhum trabalho a fazer. Esse também é o mecanismo que o React Compiler automatiza: ele faz cache dos próprios objetos de elemento, produzindo o mesmo bailout sem a reestruturação.

![Benchmark mostrando ExpensiveList renderizando só uma vez depois de ser elevado para children, ao longo de 100 teclas](https://victorleao.dev/images/articles/react-tree-optimization/children-as-props-good.png)

*Mesmas 100 teclas digitadas: média de 0.08ms, 7.70ms no total. StatefulInput renderiza 100 vezes. ExpensiveList renderiza uma vez — uma diferença de 4.492%. Sem memo.*

> SearchLayout não pode ter estado próprio. Se tiver, ele também re-renderiza — e cria um novo elemento `<ExpensiveList />` a cada render, destruindo a estabilidade de referência da qual o padrão depende. A estabilidade do escopo do componente externo é o mecanismo. Remova essa estabilidade — adicione estado a SearchLayout — e a otimização desaparece.

> **Lifting content up**
>
> O componente com estado aceita seus filhos caros de fora, em vez de criá-los inline. Ele gerencia comportamento; o pai controla o conteúdo. A fronteira do re-render fica explícita na estrutura.

> Experimente os dois padrões você mesmo no benchmark interativo.

## A árvore como decisão de design

Dois cenários, duas causas diferentes, duas correções diferentes. O padrão subjacente é idêntico em cada um: uma decisão estrutural — onde o estado mora, quem cria um componente — determina quais componentes re-renderizam quando algo muda. Acertar essa decisão elimina o problema. Errá-la cria um problema de performance que o `memo` consegue remendar, mas não resolver.

**O estado pertence ao seu ancestral comum mais próximo — não ao mais conveniente.** Todo nível acima disso é propagação desnecessária. Todo nível desnecessário é um lugar onde o `memo` eventualmente vai aparecer e ninguém vai lembrar por quê.

**Frequência de atualização é uma dimensão de design.** Um estado que muda a cada tecla e um estado que muda quando um modal abre têm custos de propagação completamente diferentes. Misturá-los no mesmo componente significa que o estado que muda rápido força re-renders em tudo que está downstream do que muda devagar também. A árvore deveria refletir taxas de mudança — não quais funcionalidades parecem pertencer juntas na tela.

**Memoização é um sinal, não uma solução.** Recorrer ao `React.memo` é reconhecer que um componente re-renderiza mais do que deveria, e que reestruturar não vale a pena agora. Às vezes essa é a decisão certa. Mas é um remendo — e remendos se acumulam. Uma base de código coberta de wrappers `memo` é uma base de código cheia de decisões estruturais que nunca foram de fato tomadas.

O React Compiler remove os wrappers, não as decisões — e remove o sintoma que costumava avisar que elas estavam pendentes. Uma árvore com estado no lugar errado deixa de ser lenta. Ainda é a árvore onde um valor passa por cinco componentes que nunca o leem, onde mover uma funcionalidade significa rastrear qual ancestral é dono do seu estado, e onde um novo engenheiro lê quatro arquivos pra responder uma pergunta que a estrutura deveria ter respondido. O compilador torna isso rápido. Não torna isso legível.

## Partindo da árvore

A mudança que este artigo pede é sobre a ordem das perguntas.

Antes de abrir o profiler, antes de recorrer ao `memo`, pergunte: onde precisa morar o estado que está causando esse re-render? Se está mais alto do que seu consumidor mais próximo, mova para baixo. Se não pode ser movido, o componente caro pode ser passado como `children` a partir de um escopo externo estável?

Se você quer testar essa pergunta antes de escrever um único componente, o [Treeact](https://www.treeact.dev/) deixa você esboçar a árvore, marcar onde estado e children moram, e assistir a cascata de re-render antes que ela exista em código.

Essas perguntas são mais rápidas de responder do que depuração posterior. Elas deixam a base de código mais simples do que a encontraram. E elas endereçam a causa — não o sintoma.

A parte difícil não é aprender os padrões. É construir o hábito de tratar a árvore de componentes como uma decisão de design de primeira classe: algo em que você pensa antes de escrever um componente, não algo que você remenda depois que o profiler te avisa que algo está lento.

A maioria das árvores não é projetada. É acumulada.

> Projete a árvore. `memo` é o que acontece quando você não projeta.
