DEV Community

Lucas Henrique Alves Rosa
Lucas Henrique Alves Rosa

Posted on

O que é o DOM e o que o React realmente faz com ele?

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>
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

  • count guarda o dado.
  • counter.textContent apresenta 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);
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

Depois de alguns cliques:

console.log(
  window.counterBefore === document.querySelector("#counter")
); // true
Enter fullscreen mode Exit fullscreen mode

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`;
}
Enter fullscreen mode Exit fullscreen mode

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`;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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:

  1. Abra o DevTools e a aba Performance.
  2. Comece uma gravação.
  3. Clique no contador algumas vezes, incluindo a passagem de 9 para 10.
  4. Pare a gravação.
  5. 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?

Referências


Top comments (0)