O que é o DOM e o que o React realmente faz com ele?
Este é o primeiro artigo de uma série que quero escrever: “Coisas que usamos todos os dias, mas raramente paramos para entender.”
A ideia é escolher conceitos que aparecem no meu trabalho, estudar com mais profundidade e registrar o que aprendi. Escrever também é uma forma de testar meu entendimento: quando tento explicar alguma coisa, encontro dúvidas que antes passavam despercebidas.
Para começar, escolhi algo mais canônico no desenvolvimento web: o DOM.
O DOM (Document Object Model, ou Modelo de Objeto de Documento) é uma representação do documento como uma árvore de objetos e nós. Por meio das suas APIs, linguagens como JavaScript podem ler e modificar o conteúdo e a estrutura da página, além de alterar atributos e estilos dos elementos.
Vamos pegar um exemplo bem simples. Imagine que você vai fazer seu segundo programa, depois do “Hello, World!”: um botão que conta cliques.
Cabe uma menção honrosa ao Egg Clicker, pela ideia de tocar em um ovo e acompanhar um contador. Aqui, vamos seguir essa mesma ideia básica e caminhar do HTML até o botão funcionando, com o número de cliques aparecendo na página.
1. Do HTML para uma árvore de objetos
Considere este HTML:
<body> <main> <h1>Contador</h1> <p>Cliques: <span id="counter">0</span></p> <button id="button">Incrementar</button> </main> </body> O navegador interpreta a marcação — processo chamado de parsing — e constrói uma estrutura em memória. Essa estrutura é o DOM.
Mas o que significa dizer que ele é uma “árvore de objetos e nós”?
O que é um nó?
Um nó é uma unidade dessa estrutura. Um elemento como main é um nó. O texto dentro de um elemento também é um nó. Comentários também podem ser nós.
No contador:
-
<span id="counter">corresponde a um nó de elemento. - O texto
"0"dentro dele corresponde a um nó de texto. - O
idé um atributo do elemento; não é um filho na árvore.
Portanto, todo elemento é um nó, mas nem todo nó é um elemento.
E onde entram os objetos?
O navegador expõe esses nós como objetos, com propriedades e métodos. Quando fazemos:
const counter = document.querySelector("#counter"); A variável counter recebe uma referência ao objeto que representa aquele elemento, não uma cópia da sua marcação HTML.
Podemos consultar esse objeto:
console.log(counter.tagName); // "SPAN" console.log(counter.id); // "counter" console.log(counter.textContent); // "0" console.log(counter.parentElement); // o elemento <p> E por que uma árvore?
Porque os nós têm relações de parentesco: um nó pode conter filhos, e cada filho ocupa uma posição nessa hierarquia.
O main contém h1, p e button. O p contém o texto “Cliques:” e o span. O span contém o texto “0”.
Este mapa mostra a parte principal do exemplo:
flowchart TD body["Elemento: body"] --> main["Elemento: main"] main --> h1["Elemento: h1"] main --> p["Elemento: p"] main --> button["Elemento: button"] h1 --> title["Texto: Contador"] p --> label["Texto: Cliques:"] p --> span["Elemento: span — id counter"] span --> value["Texto: 0"] button --> action["Texto: Incrementar"] O mapa simplifica a árvore: omite os nós de texto de espaços e quebras de linha. No documento completo, também temos Document, o doctype, html e head.
O DOM é a estrutura inteira; os nós são suas partes; a árvore é a organização dessas partes; e os objetos são a forma como acessamos os nós pelo código.
Para observar a diferença entre filhos que são elementos e filhos que podem ser qualquer tipo de nó, execute no console:
const paragraph = document.querySelector("p"); console.log(paragraph.children); // somente elementos filhos console.log(paragraph.childNodes); // inclui nós de texto const counter = document.querySelector("#counter"); console.log(counter.nodeType); // 1: nó de elemento console.log(counter.firstChild.nodeType); // 3: nó de texto console.log(counter.firstChild.nodeValue); // "0", antes dos cliques Modificar o objeto muda o documento em memória
O DOM oferece APIs para consultar e modificar essa estrutura:
const counter = document.querySelector("#counter"); counter.textContent = "10"; Não estamos editando o arquivo HTML original. Estamos modificando a representação do documento que o navegador mantém em memória.
Outro detalhe: o DOM não faz parte da linguagem JavaScript. É uma API disponibilizada pelo navegador. Por isso, document.querySelector está disponível numa página, mas não faz parte do ambiente padrão do Node.js.
2. Vamos reproduzir o contador com JavaScript
Crie um arquivo chamado index.html, cole este código e abra o arquivo no navegador:
<!doctype html> <html lang="pt-BR"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Contador com JavaScript</title> </head> <body> <main> <h1>Contador</h1> <p> Cliques: <span id="counter" aria-live="polite">0</span> </p> <button id="button" type="button">Incrementar</button> </main> <script> const button = document.querySelector("#button"); const counter = document.querySelector("#counter"); let count = 0; button.addEventListener("click", () => { count += 1; counter.textContent = String(count); }); </script> </body> </html> O script aparece depois dos elementos que buscamos. Assim, quando ele executa, o navegador já construiu essa parte do DOM.
Mantive o valor em uma variável, em vez de incrementar diretamente textContent, para separar duas responsabilidades:
-
countguarda o dado. -
counter.textContentapresenta esse dado na interface.
O aria-live="polite" permite que tecnologias assistivas anunciem a atualização sem interromper imediatamente o que está sendo lido.
Agora clique algumas vezes no botão. O comportamento é simples, mas vamos observar o que aconteceu.
3. O que acontece a cada clique?
addEventListener registra uma função para responder ao evento click. Quando o botão recebe o evento, a função executa:
count += 1; counter.textContent = String(count); A primeira linha modifica uma variável JavaScript. A segunda modifica o DOM.
Se você remover a segunda linha, o valor continuará aumentando na memória, mas a página continuará mostrando 0.
Alterar o dado e atualizar sua representação na interface são coisas diferentes. Neste exemplo, fazemos essa ligação manualmente.
Abra o DevTools, encontre o span na aba Elements/Elementos e clique no botão. Você verá seu conteúdo mudando.
Podemos confirmar que o elemento foi preservado. Antes de clicar, execute no console:
window.counterBefore = document.querySelector("#counter"); Depois de alguns cliques:
console.log( window.counterBefore === document.querySelector("#counter") ); // true O objeto do elemento continua sendo o mesmo. Seu conteúdo mudou.
Isso não significa que todos os seus filhos foram preservados: atribuir textContent a um elemento substitui seu conteúdo por texto. É seguro aqui porque o span foi criado apenas para mostrar o número.
4. Como a mudança chega à tela?
O DOM representa o documento, mas não é uma imagem pronta.
O navegador também considera o CSS e realiza o trabalho necessário para apresentar a página:
- Cálculo de estilos: determina as regras aplicáveis.
- Layout: calcula tamanhos e posições.
- Paint: desenha textos, fundos, bordas e outros detalhes.
- Composição: combina as camadas que formam o resultado visual.
Nem toda atualização exige todas essas etapas. O trabalho depende da mudança e da estrutura da página.
Trocar o texto do contador pode afetar a largura do conteúdo e exigir layout. Já outras alterações podem não modificar a geometria dos elementos.
Além disso, o navegador pode agrupar atualizações antes de apresentar o próximo quadro. Não existe uma relação obrigatória de “uma escrita no DOM = um desenho imediato”.
5. Por que isso pode ficar caro?
O nosso contador é pequeno. O problema aparece quando a atualização exige muito trabalho ou quando obrigamos o navegador a repeti-lo.
Um exemplo é intercalar leituras de medidas e alterações de estilo:
const cards = document.querySelectorAll(".card"); const container = document.querySelector(".container"); for (const card of cards) { card.style.width = `${container.offsetWidth}px`; } Dependendo do layout, a escrita pode invalidar cálculos anteriores. Na próxima volta, a leitura de offsetWidth pode forçar um novo cálculo para obter a medida atualizada.
Esse padrão pode causar layout thrashing: cálculos de layout forçados repetidamente.
Se queremos a largura do container antes das alterações, podemos ler uma vez e depois escrever:
const cards = document.querySelectorAll(".card"); const container = document.querySelector(".container"); const width = container.offsetWidth; for (const card of cards) { card.style.width = `${width}px`; } Essa reorganização faz sentido quando a medida capturada é a desejada para todos os cards. Se as escritas mudarem a largura do container e precisarmos de medidas intermediárias, o comportamento será diferente.
O objetivo é entender o custo das operações e medir quando houver um problema real.
6. O mesmo contador em React
Em um projeto React existente, substitua src/App.jsx por este código. Outra opção é usar um ambiente online com template React:
import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return ( <main> <h1>Contador</h1> <p> Cliques: <span id="counter" aria-live="polite">{count}</span> </p> <button type="button" onClick={() => setCount((current) => current + 1)} > Incrementar </button> </main> ); } O resultado é o mesmo, mas a responsabilidade mudou.
No JavaScript puro, dizemos: “Encontre este elemento e altere seu conteúdo”.
No React, descrevemos: “Para este estado, a interface deve mostrar este conteúdo”.
A primeira abordagem é imperativa. A segunda é declarativa.
JSX descreve a interface; não é uma string de HTML que o React simplesmente cola na página.
7. Renderização, commit e navegador
Ao clicar, setCount solicita uma atualização de estado. A função current => current + 1 calcula o próximo valor a partir do estado pendente.
O React então calcula a próxima interface executando o componente. Essa etapa é a renderização do React.
Depois, na etapa de commit, o React DOM aplica as alterações necessárias ao DOM real.
No contador, o texto muda, e os elementos podem ser preservados. O navegador realiza o trabalho visual necessário para mostrar a atualização.
Por isso, renderizar novamente não significa recriar o DOM inteiro.
O termo Virtual DOM costuma se referir à representação da interface mantida em memória. A reconciliação compara a nova descrição com a anterior para determinar as atualizações.
Isso não garante que React seja mais rápido que qualquer implementação manual. Ele também executa código e faz comparações. O benefício está em coordenar a relação entre estado e interface, especialmente conforme a aplicação cresce.
8. Algumas perguntas interessantes
O componente renderizou novamente?
No contador React, sim: ao incrementar o estado, o React executa o componente novamente para calcular a interface.
No exemplo em JavaScript puro, não existe componente React. Existe a execução do handler e a atualização direta do DOM.
Para observar a execução do componente, acrescente temporariamente um log dentro da função, antes do return:
console.log("App executou com count:", count); Esse log indica uma execução, não prova que houve mudança no DOM ou uma imagem nova na tela. Em desenvolvimento, o Strict Mode pode executar o componente mais de uma vez para detectar problemas. Tentativas de renderização também podem ser descartadas.
Para analisar renderizações com mais detalhe, use o Profiler do React DevTools.
O DOM mudou?
Sim, nos dois contadores: o conteúdo apresentado pelo span passa de 0 para 1, depois para 2, e assim por diante.
No React, uma nova renderização também pode produzir o mesmo resultado e não exigir alterações no DOM.
Para observar as mutações no nosso exemplo, execute este código no console antes de clicar:
const target = document.querySelector("#counter"); window.counterObserver = new MutationObserver((records) => { for (const record of records) { console.log("Mutação:", record.type, "Valor:", target.textContent); } }); window.counterObserver.observe(target, { subtree: true, childList: true, characterData: true, }); As opções incluem mudanças nos filhos e nos nós de texto, cobrindo formas diferentes de atualizar esse conteúdo. O callback recebe notificações de maneira assíncrona: ele não mede quanto tempo levou para desenhar a tela.
Para encerrar a observação:
window.counterObserver.disconnect(); Você também pode repetir a comparação de referência da seção 3 para confirmar que o elemento span permaneceu o mesmo.
Houve cálculo de layout?
Pode haver, mas precisamos observar a execução para afirmar. O novo número pode modificar a geometria do texto, especialmente em uma passagem como 9 para 10.
Um layout também pode ocorrer mesmo sem uma mutação no DOM, por exemplo quando a janela muda de tamanho.
Para investigar no Chrome:
- Abra o DevTools e a aba Performance.
- Comece uma gravação.
- Clique no contador algumas vezes, incluindo a passagem de 9 para 10.
- Pare a gravação.
- Amplie o intervalo dos cliques e procure eventos como Recalculate Style, Layout e Paint na thread principal.
A aba Elements mostra a estrutura. Ela, sozinha, não informa se houve layout.
Qual dessas etapas está consumindo tempo?
Não dá para concluir apenas lendo o código ou contando renderizações. Precisamos medir a interação.
No contador, esperamos pouco trabalho, mas isso é uma expectativa, não um resultado de benchmark.
Use duas perspectivas:
| O que investigar | Ferramenta | O que observar |
|---|---|---|
| Cálculo dos componentes React | React DevTools Profiler | Componentes renderizados e duração do trabalho de renderização |
| JavaScript, estilos, layout e desenho | DevTools Performance | Eventos, duração e pilha de chamadas no intervalo da interação |
O Profiler do React não mede sozinho todo o custo de layout e paint do navegador. Para medir programaticamente a renderização de uma subárvore, React também oferece o componente Profiler e o campo actualDuration; ele não representa o tempo total até a imagem aparecer.
Se o trabalho estiver concentrado no JavaScript, investigamos o handler e os cálculos dos componentes. Se houver layouts longos ou repetidos, investigamos geometria e leituras de medidas. Se paint dominar, investigamos o trabalho de desenho.
Logs e observadores são úteis para entender o mecanismo, mas interferem nas medições. Remova a instrumentação didática e considere as diferenças entre desenvolvimento e produção antes de tirar conclusões sobre desempenho.
Conclusão
A parte mais interessante desse contador foi separar coisas que parecem acontecer ao mesmo tempo.
O JavaScript muda o dado. O DOM representa o documento. O React calcula a interface e coordena suas atualizações. O navegador calcula e desenha o resultado visual.
Um componente pode renderizar sem mudar o DOM. O DOM pode mudar sem refazer toda a página. E uma mudança pequena pode exigir trabalho de layout dependendo do contexto.
Entender essas diferenças me ajuda a investigar um problema com mais precisão, em vez de atribuir qualquer lentidão ao “React renderizando demais”.
É essa a proposta da série: pegar algo cotidiano, reproduzir um exemplo e registrar o que existe por trás.
Você já precisou investigar uma renderização ou uma atualização do DOM que estava deixando a interface lenta?

