Re-render no React: por que o componente roda de novo
O que dispara uma nova renderização, o que o React faz com o resultado e por que renderizar de novo não é a mesma coisa que mexer no DOM.
Renderizar, no React, é executar a função do componente e receber de volta a descrição da tela. Só isso. O React compara essa descrição com a anterior e, se achar diferença, toca no DOM. Rodar de novo é barato; mexer no DOM é que custa — e as duas coisas quase nunca acontecem na mesma proporção.
Todos os exemplos aqui são o mesmo painel: a tela de acompanhamento da Lavanderia Água Viva, com a lista de ordens de serviço, um contador de ciclos concluídos no cabeçalho e um rodapé fixo. Cada número que aparece nesta página saiu de uma execução real no Node 24.16.0, com React 19.2.8 renderizando dentro do jsdom 30.0.1 — o mesmo React que roda no navegador, só que num DOM que eu consigo instrumentar e contar.
Refazer a planta não é reformar a casa
Um arquiteto pode recalcular a planta inteira e descobrir que só uma placa da recepção mudou. Recalcular a planta é trabalho; quebrar parede e mover móvel é outro, bem mais caro. Misturar os dois faz qualquer nova execução da função parecer uma reforma completa.
No React, render é executar componentes para produzir uma nova descrição; commit é aplicar ao DOM apenas as diferenças necessárias. Use os contadores do primeiro experimento como microprática: compare quantas vezes a função roda com quantas alterações reais aparecem no DOM. Os números provam onde o custo aconteceu.
O JSX que você escreve não é HTML e não é o componente. Ele vira uma chamada de função que devolve um objeto simples, com o tipo e as props. Esse objeto é o elemento: uma receita, não o prato.
import { createRoot } from 'react-dom/client';
const ORDENS = [
{ id: 1, cliente: 'Marina Alves', pecas: 12 },
{ id: 2, cliente: 'Douglas Prado', pecas: 4 },
];
function CardOrdem({ ordem }) {
console.log('CardOrdem rodou para', ordem.cliente);
return <li>#{ordem.id} {ordem.cliente} — {ordem.pecas} peças</li>;
}
function ListaOrdens({ ordens }) {
console.log('ListaOrdens rodou');
return <ul>{ordens.map((o) => <CardOrdem key={o.id} ordem={o} />)}</ul>;
}
const elemento = <ListaOrdens ordens={ORDENS} />;
console.log('o que o JSX virou:', { type: elemento.type.name, props: elemento.props });
createRoot(document.getElementById('root')).render(elemento);
console.log('HTML no navegador:', document.getElementById('root').innerHTML);Repare na ordem. O elemento existiu antes de qualquer console.log de
dentro dos componentes: criar o elemento não executa nada. Quem executa é o
React, na hora do render, e ele desce de cima para baixo — pai primeiro, filhos
depois.
Guarde essa distinção, porque ela sustenta o artigo inteiro:
| etapa | o que acontece | custo |
|---|---|---|
| criar o elemento | o JSX vira { type, props } |
um objeto em memória |
| render | o React chama a função do componente | executar JavaScript |
| reconciliação | compara o resultado com o da vez anterior | comparar objetos |
| commit | aplica no DOM só a diferença | tocar na página |
As três coisas que disparam um render
Um componente roda de novo em três situações, e só nelas: o estado dele mudou, o pai renderizou, ou um contexto que ele consome mudou.
Este trecho tem uma Bancada com estado de turno, uma Maquina com uma
Etiqueta dentro, e um Cesto com estado próprio:
const TurnoContexto = createContext('manhã');
function Etiqueta() {
const turno = useContext(TurnoContexto);
return <span>turno: {turno}</span>;
}
function Maquina({ nome }) {
return <div><b>{nome}</b><Etiqueta /></div>;
}
function Cesto() {
const [pecas, setPecas] = useState(0);
return <p>peças no cesto: {pecas}</p>;
}
function Bancada() {
const [turno, setTurno] = useState('manhã');
return (
<TurnoContexto value={turno}>
<Maquina nome="Lavadora 1" />
<Cesto />
</TurnoContexto>
);
}Chamando cada setter de fora, por um roteiro de teste, e anotando quem rodou:
Três leituras. O estado do Cesto mudou e só o Cesto rodou: o irmão não sabe
de nada, e o pai também não. O estado da Bancada mudou e a árvore inteira
abaixo dela rodou. E setPecas(3) com pecas já valendo 3 não disparou
render nenhum — o React compara o valor novo com o antigo usando Object.is e
desiste antes de começar. É por isso que atualizar estado sem criar valor novo
não repinta a tela, assunto de
atualizar estado no React sem mutar array e objeto.
O terceiro gatilho é o mais escorregadio, porque ele atravessa qualquer
barreira. Aqui a Maquina está envolvida em memo, então o render do pai não
chega nela — mas a Etiqueta, lá dentro, consome o contexto:
const Maquina = memo(function Maquina({ nome }) {
return <div><b>{nome}</b><Etiqueta /></div>;
});A Maquina foi pulada e a Etiqueta, que está dentro dela, rodou assim mesmo.
O Cesto rodou pelo gatilho anterior — o pai renderizou. Contexto não desce
pela árvore de props: quando o valor do provedor muda, o React percorre a árvore
a partir dele procurando quem declarou depender daquele contexto, e essa varredura
atravessa o memo em vez de parar nele. Se isso te pegou de surpresa, vale ler
Context API no React com esse detalhe em mente.
O que o React compara antes de encostar no DOM
Aqui está o mal-entendido que faz gente otimizar o que não precisa. “O componente renderizou de novo” não quer dizer “a página foi reescrita”.
Montei o painel completo — cabeçalho com contador, ListaOrdens com seis
CardOrdem e Rodape — e coloquei um MutationObserver no elemento raiz para
contar quantas alterações reais chegaram ao DOM depois de um clique no botão
“Registrar ciclo”:
// as seis ordens do dia; cada função de componente do painel faz execucoes++
const ORDENS = [
{ id: 1, cliente: 'Marina Alves', pecas: 12, status: 'lavando' },
{ id: 2, cliente: 'Douglas Prado', pecas: 4, status: 'secando' },
{ id: 3, cliente: 'Heloísa Bastos', pecas: 9, status: 'pronto' },
{ id: 4, cliente: 'Marcos Tanaka', pecas: 21, status: 'lavando' },
{ id: 5, cliente: 'Cláudia Ferraz', pecas: 6, status: 'pronto' },
{ id: 6, cliente: 'Ubirajara Melo', pecas: 3, status: 'secando' },
];
let execucoes = 0;
const raiz = document.getElementById('root');
createRoot(raiz).render(<Painel />);
const liAntes = [...raiz.querySelectorAll('li')];
execucoes = 0;
const registros = [];
const obs = new MutationObserver((lista) => registros.push(...lista));
obs.observe(raiz, {
childList: true,
subtree: true,
characterData: true,
characterDataOldValue: true,
attributes: true,
});
raiz.querySelector('button').click();
obs.disconnect();
console.log('funções de componente executadas:', execucoes);
console.log('mutações no DOM:', registros.length);
for (const r of registros) {
const pai = r.target.parentNode.nodeName.toLowerCase();
console.log(` ${r.type} em <${pai}>: "${r.oldValue}" -> "${r.target.data}"`);
}
const liDepois = [...raiz.querySelectorAll('li')];
console.log(
'os 6 <li> continuam sendo os mesmos nós?',
liAntes.every((n, i) => n === liDepois[i]),
);Nove funções de componente executaram. Um nó de texto mudou na página. Os
seis <li> da lista continuam sendo exatamente os mesmos objetos de DOM que
existiam antes do clique — nenhum foi destruído, nenhum foi recriado.
O que o React faz entre uma coisa e outra é a reconciliação: ele percorre a árvore nova ao lado da antiga e, para cada posição, pergunta se o tipo é o mesmo. Se for, reaproveita o nó do DOM e só ajusta o que mudou. Se não for, joga fora e monta de novo.
Dá para ver isso acontecendo. O mesmo <input>, na mesma posição, com o
envólucro mudando de div para section:
function FichaOrdem({ trocarTipo }) {
const [compacto, setCompacto] = useState(false);
const Envolucro = trocarTipo && compacto ? 'section' : 'div';
return (
<Envolucro>
<strong>Ordem #4 — Marcos Tanaka {compacto ? '(compacta)' : ''}</strong>
<input placeholder="observação da ordem" />
</Envolucro>
);
}Trocando só a tag do envólucro, o <input> foi destruído e recriado, e a
observação que a atendente tinha digitado sumiu. É a mesma regra que faz uma
key instável apagar o que o usuário escreveu — o assunto de
renderizar listas no React com map e a prop key.
O pai renderiza e o filho vai junto, mesmo sem props novas
Volte ao painel completo. O botão “Registrar ciclo” mexe num número que só o cabeçalho mostra. A lista de ordens não depende dele em nada. Mesmo assim:
function Painel() {
const [ciclos, setCiclos] = useState(0);
return (
<div>
<header>
<strong>Ciclos concluídos hoje: {ciclos}</strong>
<button onClick={() => setCiclos(ciclos + 1)}>Registrar ciclo</button>
</header>
<ListaOrdens ordens={ORDENS} />
<Rodape />
</div>
);
}O clique produziu exatamente o mesmo trabalho da montagem inicial: nove
execuções. ORDENS é a mesma constante, as props de cada card são idênticas, o
Rodape não recebe prop nenhuma — e ainda assim todo mundo rodou.
Isso não é um defeito. É a regra que torna o React previsível: quando um componente renderiza, o React descarta a saída anterior dele inteira e usa a nova. Como a saída nova contém elementos novos para os filhos, cada filho precisa ser executado para produzir a própria saída. Por padrão o React não tenta adivinhar que a saída daria igual.
Dez caixas na árvore e nove execuções: o cabeçalho é markup dentro do Painel,
não um componente com função própria. Numa lista de seis ordens isso é
irrelevante. Numa lista de mil, com formatação de moeda em cada linha, deixa de
ser.
React.memo: onde a comparação para de subir
React.memo envolve um componente e instala uma pergunta antes da execução: as
props são as mesmas da vez passada? Se forem — comparação rasa, campo a campo,
com Object.is —, o React reaproveita a saída anterior e não desce mais por
ali.
const CardOrdem = memo(function CardOrdem({ ordem }) {
return <li>#{ordem.id} {ordem.cliente} — {ordem.pecas} peças ({ordem.status})</li>;
});
const ListaOrdens = memo(function ListaOrdens({ ordens }) {
return <ul>{ordens.map((o) => <CardOrdem key={o.id} ordem={o} />)}</ul>;
});
const Rodape = memo(function Rodape() {
return <footer>Lavanderia Água Viva — 6 máquinas</footer>;
});De nove execuções para uma. Note que o memo em ListaOrdens já resolveu
sozinho: como ela foi pulada, os seis cards nem chegaram a ser considerados. O
memo não é um filtro em cada componente — é uma barreira que interrompe a
descida naquele ponto da árvore.
O problema é que a barreira tem um vidro fácil de quebrar. Basta uma prop que nasce nova a cada render — um objeto literal, um array recém-filtrado, uma arrow function escrita direto no JSX:
// CardOrdem é o mesmo de cima, envolvido em memo, agora recebendo aoEntregar
function PainelFurado() {
const [ciclos, setCiclos] = useState(0);
return (
<div>
<button onClick={() => setCiclos(ciclos + 1)}>ciclos: {ciclos}</button>
<ul>
{ORDENS.map((o) => (
<CardOrdem key={o.id} ordem={o} aoEntregar={(id) => console.log(id)} />
))}
</ul>
</div>
);
}
function PainelCorrigido() {
const [ciclos, setCiclos] = useState(0);
const aoEntregar = useCallback((id) => console.log(id), []);
return (
<div>
<button onClick={() => setCiclos(ciclos + 1)}>ciclos: {ciclos}</button>
<ul>
{ORDENS.map((o) => (
<CardOrdem key={o.id} ordem={o} aoEntregar={aoEntregar} />
))}
</ul>
</div>
);
}A arrow é uma função nova em cada execução do pai, e Object.is de duas funções
diferentes dá false. O memo está lá, roda a comparação, erra, e renderiza
tudo de novo — pagando a comparação de graça. Trocando por um useCallback com
lista de dependências vazia, a mesma referência sobrevive e os seis cards são
pulados. É esse o papel real do
useMemo e useCallback: eles não pulam render,
eles alimentam quem pula.
Empurrar o estado para baixo, e o truque do children
Antes de sair espalhando memo, existe uma correção mais barata: colocar o
estado onde ele é usado. O contador de ciclos só interessa ao cabeçalho.
Então o estado deveria morar num componente do tamanho do cabeçalho:
function ContadorCiclos() {
const [ciclos, setCiclos] = useState(0);
return (
<header>
<strong>Ciclos concluídos hoje: {ciclos}</strong>
<button onClick={() => setCiclos(ciclos + 1)}>Registrar ciclo</button>
</header>
);
}
function Painel() {
return (
<div>
<ContadorCiclos />
<ListaOrdens ordens={ORDENS} />
<Rodape />
</div>
);
}Uma execução, sem memo nenhum, sem useCallback, sem lista de dependências
para manter. O Painel não tem estado, então ele não roda de novo; e como ele
não roda, ninguém abaixo dele roda.
Nem sempre dá para descer o estado — às vezes o valor precisa mesmo ficar por
cima. Nesse caso existe uma saída que quase ninguém usa: passar o que não
depende do estado como children.
function ProvedorCiclos({ children }) {
const [ciclos, setCiclos] = useState(0);
return (
<div>
<header>
<strong>Ciclos concluídos hoje: {ciclos}</strong>
<button onClick={() => setCiclos((c) => c + 1)}>Registrar ciclo</button>
</header>
{children}
</div>
);
}
function Painel() {
return (
<ProvedorCiclos>
<ListaOrdens ordens={ORDENS} />
<Rodape />
</ProvedorCiclos>
);
}De novo: uma execução, zero memo. O motivo é bonito. Os elementos de
<ListaOrdens /> e <Rodape /> foram criados dentro do Painel, que não
renderizou. Quando o ProvedorCiclos roda, ele devolve o mesmo objeto de
children que já tinha; o React vê elemento idêntico na mesma posição e pula.
Essa é a base da composição que
children e composição no React explora.
Os quatro arranjos do mesmo painel, medidos com o mesmo clique no mesmo botão:
| arranjo | funções que rodaram | mutações no DOM | custo em código |
|---|---|---|---|
estado no Painel, sem memo |
9 | 1 | nenhum |
estado no Painel, memo nos três filhos |
1 | 1 | três memo e cuidado com props |
estado no ContadorCiclos |
1 | 1 | nenhum |
estado no provedor, resto como children |
1 | 1 | uma indireção |
A leitura dessa tabela é a opinião central deste artigo: memo é a terceira
opção, não a primeira. Mover estado e compor com children chegam ao mesmo
resultado sem criar uma regra que alguém vai quebrar daqui a três sprints ao
adicionar uma prop nova.
Contando renders sem instalar plugin nenhum
Antes de otimizar, meça. Não precisa de extensão: um useRef que incrementa a
cada execução já entrega a contagem, porque useRef sobrevive entre renders sem
disparar render.
function useContarRenders(nome) {
const renders = useRef(0);
renders.current += 1;
console.log(`${nome} rodou ${renders.current}x`);
return renders.current;
}
function CardOrdem({ ordem }) {
useContarRenders(`CardOrdem #${ordem.id}`);
return <li>{ordem.cliente}</li>;
}
function Painel() {
useContarRenders('Painel');
const [ciclos, setCiclos] = useState(0);
return (
<div>
<button onClick={() => setCiclos(ciclos + 1)}>ciclos: {ciclos}</button>
<ul>{ORDENS.slice(0, 2).map((o) => <CardOrdem key={o.id} ordem={o} />)}</ul>
</div>
);
}Agora o detalhe que faz muita gente diagnosticar errado. O mesmo código, dentro
de <StrictMode> — que o Vite coloca por padrão no main.jsx:
Cada número dobrou. Isso é o StrictMode executando a função do componente duas
vezes de propósito, no desenvolvimento, para revelar quem faz efeito colateral
durante a renderização. Não acontece em produção. Ao contar renders, divida por
dois — ou compare cenários entre si, que é o que importa.
Renderização em série e sem parar é outra história: quando a contagem sobe
sozinha até o navegador travar, o problema é setState chamado durante o
render, e o React avisa com
Maximum update depth exceeded.
Onde o tempo realmente vai, medido com o Profiler
Contagem de execuções é útil, mas não é tempo. Para tempo, o React expõe o
componente <Profiler>, que entrega a duração real de cada commit.
const STATUS = ['lavando', 'secando', 'passando', 'pronto'];
const fazerOrdens = (n) =>
Array.from({ length: n }, (_, i) => ({
id: i + 1,
cliente: `Cliente ${i + 1}`,
pecas: (i % 17) + 1,
status: STATUS[i % 4],
valor: 8.5 * ((i % 17) + 1),
}));
function CardOrdem({ ordem }) {
const valor = ordem.valor.toLocaleString('pt-BR', {
style: 'currency',
currency: 'BRL',
});
return <li>#{ordem.id} {ordem.cliente} — {ordem.pecas} peças · {valor} ({ordem.status})</li>;
}
const CardOrdemMemo = memo(CardOrdem);
function Painel({ ordens, Card }) {
const [ciclos, setCiclos] = useState(0);
return (
<div>
<button onClick={() => setCiclos((c) => c + 1)}>ciclos: {ciclos}</button>
<ul>{ordens.map((o) => <Card key={o.id} ordem={o} />)}</ul>
</div>
);
}
async function medir(elemento, cliques = 25) {
const raiz = document.createElement('div');
document.body.appendChild(raiz);
const duracoes = [];
const root = createRoot(raiz);
await act(() =>
root.render(
<Profiler
id="painel"
onRender={(id, fase, duracao) => {
if (fase === 'update') duracoes.push(duracao);
}}
>
{elemento}
</Profiler>,
),
);
const botao = raiz.querySelector('button');
for (let i = 0; i < cliques; i++) await act(() => botao.click());
await act(() => root.unmount());
const amostras = duracoes.slice(5); // os 5 primeiros são aquecimento
return amostras.reduce((s, n) => s + n, 0) / amostras.length;
}
for (const n of [6, 50, 400, 2000]) {
const ordens = fazerOrdens(n);
const sem = await medir(<Painel ordens={ordens} Card={CardOrdem} />);
const com = await medir(<Painel ordens={ordens} Card={CardOrdemMemo} />);
console.log(
`${String(n).padStart(4)} cards | sem memo ${sem.toFixed(2).padStart(6)} ms` +
` | com memo ${com.toFixed(2).padStart(6)} ms | diferença ${(sem - com).toFixed(2)} ms`,
);
}O act do React é o que garante que cada clique termine de renderizar antes do
próximo. Tirando ele daí, os 25 cliques só enfileiram trabalho e o laço acaba
antes de qualquer commit: o <Profiler> registra zero atualizações e a média sai
NaN. Rodei o mesmo painel com quatro tamanhos de lista, 25 cliques em cada,
sempre descartando os 5 primeiros como aquecimento, e comparando com e sem
memo no card:
Repeti a bateria quatro vezes. A coluna “sem memo” ficou entre 0,22 e 0,27 ms nos 6 cards, entre 1,44 e 1,57 ms nos 50, entre 7,46 e 7,70 ms nos 400 e entre 38,9 e 39,9 ms nos 2000: variação de ruído, sem nenhuma linha trocando de lugar. Ambiente: MacBook, Node 24.16.0, React 19.2.8 em modo de desenvolvimento dentro do jsdom. No navegador os valores absolutos mudam, e o commit real ainda soma layout e pintura por cima; a proporção entre as linhas é o que se aproveita.
O que a tabela diz é simples e é quase sempre ignorado: o ganho do memo cresce
com o tamanho da lista, e não existe em lista pequena. Economizar 0,17 ms num
painel de seis ordens não muda nada — 16 ms é o orçamento de um quadro a 60
quadros por segundo, e você gastou 1% disso.
O que não vale a pena otimizar
Falta a conta que quase ninguém faz: quanto custa um memo que nunca acerta.
Montei 2000 cards recebendo estilo={{ margin: 0 }} — objeto literal, novo a
cada render, comparação condenada a falhar:
const ORDENS_2000 = fazerOrdens(2000);
function CardComEstilo({ ordem, estilo }) {
return <li style={estilo}>#{ordem.id} {ordem.cliente} — {ordem.pecas} peças</li>;
}
const CardComEstiloMemo = memo(CardComEstilo);
function PainelComEstilo({ Card }) {
const [ciclos, setCiclos] = useState(0);
return (
<div>
<button onClick={() => setCiclos((c) => c + 1)}>ciclos: {ciclos}</button>
<ul>
{ORDENS_2000.map((o) => (
// objeto novo a cada render: a comparação do memo nunca dá igual
<Card key={o.id} ordem={o} estilo={{ margin: 0 }} />
))}
</ul>
</div>
);
}
const mostrar = (nome, media) =>
console.log(`${nome.padEnd(30)} média ${media.toFixed(2)} ms`);
const sem = await medir(<PainelComEstilo Card={CardComEstilo} />);
mostrar('sem memo', sem);
const com = await medir(<PainelComEstilo Card={CardComEstiloMemo} />);
mostrar('com memo (que nunca acerta)', com);
console.log(`\ndiferença: ${(com - sem).toFixed(2)} ms a mais com memo`);diferença: 1.71 ms a mais com memo
Rodei essa comparação oito vezes seguidas. O memo inútil perdeu nas oito, com
diferença entre 0,34 ms e 2,64 ms — de 4% a 34% a mais do que não ter memo
nenhum. Faz sentido: você paga a comparação de todas as props de todos os cards
e renderiza tudo do mesmo jeito. Mas repare na ordem de grandeza, que já diz
o essencial: o memo inútil é sempre um prejuízo, e mesmo assim um prejuízo de
poucos milissegundos.
O custo grande é o que não aparece no cronômetro. Uma dependência a mais para
manter em cada useCallback. Um memo que dá falsa segurança na revisão de
código. Um componente que fica difícil de alterar porque qualquer prop nova pode
furar a barreira em silêncio, sem erro, sem aviso — só a tela ficando lenta
alguns meses depois.
O critério medido neste laboratório:
| situação | o que fazer |
|---|---|
| lista com menos de ~50 itens leves | nada; renderizar de novo é mais barato que a conversa sobre isso |
| estado que só um pedaço da tela usa | mover o estado para esse pedaço |
| estado que precisa ficar em cima | passar o resto como children |
| lista grande com trabalho por item | memo no item, e useCallback em toda função que virar prop |
| cálculo caro dentro do componente | useMemo no cálculo, antes de pensar em memo no componente |
| digitação travando o formulário | isolar o campo no próprio componente antes de qualquer outra coisa |
Duas armadilhas fecham a lista. memo num componente que recebe children é
quase sempre inútil, pelo motivo já visto. E memo num componente cujo pai
renderiza a cada tecla digitada, passando um array filtrado na hora, também é:
o array é novo, a comparação falha, e você só adicionou código.
O que vem depois
O caminho prático é este: primeiro descubra onde o estado está morando, com
o contador de useRef desta página. Se ele estiver acima de quem realmente
precisa dele, desça — muitos problemas de render desaparecem quando o estado
volta para o menor dono possível. Só depois, com o <Profiler> mostrando milissegundos de
verdade, entre com memo e useCallback no ponto exato.
Para revisar o que sustenta tudo isso, comece por useState no React, que é o gatilho de todo re-render, e depois volte ao guia completo de React para ver onde performance entra na ordem de estudo.
Perguntas frequentes
Re-render sempre deixa a tela lenta?
O StrictMode dobra os renders também em produção?
useMemo e useCallback impedem o componente de renderizar?
E o React Compiler, resolve isso sozinho?
Dá para confiar em medição feita no Node com jsdom?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0 (React 19.2.8, jsdom 30.0.1), e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — Render and Commit — react.dev
- React — Preserving and Resetting State — react.dev
- React — memo — react.dev
- React — Profiler — react.dev


