← voltar aos artigos
14/07/2026JavaScript · DOM · Frontend

Entendendo o DOM (Document Object Model): do Zero ao Avançado

Como o navegador constrói e expõe a árvore DOM, como nós, seletores, eventos e Shadow DOM funcionam e onde o desempenho realmente é gasto.

Se você usa JavaScript, React, Vue, Angular ou Rails com Stimulus, interage com o DOM todos os dias. Entendê-lo elimina confusões sobre seleção de elementos, eventos, renderização e desempenho.

HTML não é DOM

HTML é uma sintaxe de marcação que pode chegar como bytes pela rede ou existir como uma string. O DOM é o modelo vivo do documento: uma árvore de nós com interfaces e métodos que programas podem consultar e modificar.

html
<body>
  <h1>Olá, mundo!</h1>
  <button>Comprar</button>
</body>

O parser HTML transforma um fluxo de caracteres em tokens e executa um algoritmo de construção da árvore. O resultado é um objeto Document. A árvore resultante pode diferir do texto original: o parser corrige marcação inválida, insere elementos implícitos e scripts podem alterá-la depois.

plaintext
Document
└── html
    ├── head
    └── body
        ├── h1
        │   └── #text "Olá, mundo!"
        └── button
            └── #text "Comprar"

Nós, elementos e interfaces

Node é a interface básica de itens da árvore. Document, Element, Text, Comment e DocumentFragment são tipos de nó. Um elemento HTML também participa de uma cadeia de interfaces mais específica.

plaintext
EventTarget
└── Node
    └── Element
        └── HTMLElement
            └── HTMLButtonElement

O texto dentro de uma tag é um Text node separado. Espaços e quebras de linha também podem gerar nós de texto; por isso childNodes e firstChild às vezes retornam algo diferente do esperado.

APIRetornoInclui texto/comentários?Coleção
element.childrenElementos filhosNãoHTMLCollection viva
node.childNodesTodos os nós filhosSimNodeList viva
querySelectorAll()Elementos encontradosNãoNodeList estática
getElementsByClassName()Elementos encontradosNãoHTMLCollection viva

Uma coleção viva acompanha mudanças no DOM; uma coleção estática é uma fotografia do momento da consulta. Essa diferença importa quando você adiciona ou remove itens durante uma iteração.

Selecionando e navegando

javascript
const title = document.querySelector("h1");
const card = document.querySelector(".card");
const product = document.querySelector("[data-id='10']");
const cards = document.querySelectorAll(".card");

cards.forEach((item) => console.log(item));

querySelector retorna o primeiro Element compatível ou null. querySelectorAll retorna uma NodeList estática. Ambos aceitam seletores CSS; um seletor inválido lança SyntaxError.

javascript
const item = document.querySelector(".item");

item?.parentElement;
item?.children;
item?.previousElementSibling;
item?.nextElementSibling;
item?.closest(".lista");

Prefira as variantes com Element quando texto e comentários não interessam: nextSibling pode ser um Text node, enquanto nextElementSibling ignora esses nós.

Conteúdo, atributos e propriedades

javascript
const title = document.querySelector("h1");

title.textContent = "Novo título";
title.classList.add("active");
title.setAttribute("aria-live", "polite");
title.dataset.section = "intro";

textContent lê ou substitui texto sem interpretar HTML. innerText considera layout e visibilidade, podendo exigir cálculo de estilos. innerHTML serializa ou analisa marcação e deve ser usado apenas com conteúdo confiável ou sanitizado.

Atributos pertencem à marcação; propriedades pertencem aos objetos DOM. Eles frequentemente refletem um ao outro, mas não são idênticos. Em um input, por exemplo, getAttribute('value') representa o valor inicial da marcação, enquanto input.value acompanha o valor atual.

Criando, movendo e removendo nós

javascript
const list = document.querySelector("#products");
const item = document.createElement("li");

item.textContent = "Teclado";
item.className = "product";
list.append(item);

item.before("Novo: ");
item.replaceWith(item.cloneNode(true));
// item.remove() removeria o nó original se ainda estivesse conectado

Um nó só pode ocupar uma posição na árvore. Inserir um nó já conectado em outro local o move; não cria uma cópia. Para copiar, use cloneNode e decida conscientemente se os descendentes devem ser incluídos.

javascript
const fragment = document.createDocumentFragment();

for (const name of ["Mouse", "Monitor", "Cabo"]) {
  const item = document.createElement("li");
  item.textContent = name;
  fragment.append(item);
}

document.querySelector("#products").append(fragment);

DocumentFragment é um contêiner sem pai útil para montar uma subárvore antes de inseri-la. Ao anexá-lo, seus filhos são transferidos e o fragmento fica vazio. Isso reduz operações na árvore conectada, mas não é uma promessa de exatamente um layout ou uma pintura.

Quando o script pode acessar o DOM

Um script executado antes de um elemento ser analisado não o encontrará. Em scripts externos, defer normalmente é a solução: baixa em paralelo e executa depois que o documento foi analisado, preservando a ordem. Módulos também são adiados por padrão.

html
<script src="/app.js" defer></script>

DOMContentLoaded dispara quando o HTML foi completamente analisado e scripts adiados executaram; ele não espera todas as imagens. O evento load espera recursos dependentes. Não use load quando só precisa da árvore.

Eventos: captura, alvo e bubbling

Elementos implementam EventTarget. addEventListener permite vários listeners e opções como capture, once, passive e signal. Na propagação, o evento percorre captura, alvo e, quando bubbles é true, bubbling.

javascript
button.addEventListener("click", handleClick, { once: true });

function handleClick(event) {
  console.log(event.target);        // onde o evento começou
  console.log(event.currentTarget); // onde este listener está registrado
}
plaintext
Captura:  Window → Document → html → body → button
Alvo:                                      button
Bubbling: Window ← Document ← html ← body ← button

stopPropagation impede a continuação da propagação, mas não cancela a ação padrão. preventDefault tenta cancelar uma ação padrão cancelável, como a navegação de um link. stopImmediatePropagation também impede outros listeners no mesmo alvo.

Delegação de eventos

javascript
const list = document.querySelector("#products");

list.addEventListener("click", (event) => {
  const button = event.target.closest("button[data-action='buy']");

  if (!button || !list.contains(button)) return;
  buy(button.dataset.productId);
});

A delegação registra um listener em um ancestral e identifica o descendente pelo target. Ela funciona bem para listas dinâmicas e reduz o gerenciamento de listeners repetidos. closest é mais robusto que matches porque o clique pode ocorrer em um ícone dentro do botão; contains evita aceitar um elemento fora do contêiner.

Delegação não é automaticamente mais rápida em todo caso. O custo depende da frequência dos eventos, profundidade da árvore e trabalho do handler. Use-a quando simplificar o ciclo de vida ou quando muitos filhos compartilham comportamento.

Da alteração aos pixels

Alterar o DOM não significa necessariamente redesenhar a tela naquele instante. Navegadores agrupam trabalho e produzem quadros. Dependendo da mudança, o pipeline pode envolver JavaScript, cálculo de estilos, layout, paint e composição.

EtapaResponsabilidade
StyleDescobrir quais regras CSS se aplicam
LayoutCalcular tamanhos e posições geométricas
PaintDesenhar texto, cores, bordas e sombras em camadas
CompositeCombinar as camadas para formar o quadro

Nem toda mudança percorre todas as etapas. Alterar width normalmente exige layout; color costuma exigir paint, mas não layout; certas alterações de transform e opacity podem ser tratadas pela composição. O efeito exato depende do navegador e da página.

Layout forçado e layout thrashing

javascript
// Ruim: alterna escrita e leitura geométrica
for (const box of boxes) {
  box.style.width = "200px";
  console.log(box.offsetWidth);
}

// Melhor: agrupa leituras e depois escritas
const widths = boxes.map((box) => box.offsetWidth);
boxes.forEach((box, index) => {
  box.style.width = `${widths[index] * 2}px`;
});

Ler propriedades geométricas depois de invalidar estilos pode obrigar o navegador a concluir style e layout sincronamente para devolver um valor atual. Repetir esse padrão em um loop causa layout thrashing. Agrupe leituras antes das escritas e meça no painel Performance do DevTools.

  • Mantenha o DOM tão pequeno quanto a interface realmente exige.
  • Guarde referências quando a mesma seleção está em um caminho quente; querySelector, por si só, não dispara layout.
  • Agrupe alterações relacionadas; DocumentFragment ajuda em inserções de muitos nós.
  • Use requestAnimationFrame para sincronizar trabalho visual com um próximo quadro, não como cura automática.
  • Virtualize listas realmente grandes e meça antes de otimizar.

Observando mudanças com MutationObserver

javascript
const observer = new MutationObserver((records) => {
  for (const record of records) {
    console.log(record.type, record.target);
  }
});

observer.observe(document.body, {
  childList: true,
  attributes: true,
  subtree: true,
});

// Quando não for mais necessário:
observer.disconnect();

MutationObserver recebe lotes de registros sobre alterações em nós, atributos ou texto. É útil para integrar código com uma árvore modificada por terceiros, mas observar document.body inteiro sem necessidade pode gerar muito trabalho.

Shadow DOM: outra árvore, outras fronteiras

Shadow DOM permite anexar uma árvore encapsulada a um host. Seletores do documento não atravessam automaticamente essa fronteira, estilos têm regras próprias e eventos podem ser retargeted para preservar o encapsulamento. event.composedPath() mostra o caminho de propagação permitido.

javascript
const host = document.querySelector("user-card");
const shadow = host.attachShadow({ mode: "open" });

shadow.innerHTML = `<button type="button">Abrir perfil</button>`;
shadow.querySelector("button").addEventListener("click", openProfile);

Como o React se relaciona com o DOM

React não substitui o DOM. Em um render, chama componentes para calcular a interface desejada; no commit, aplica ao DOM as operações necessárias para fazê-lo corresponder ao resultado mais recente. Se nada mudou em determinado nó, React não precisa tocá-lo.

plaintext
Atualização de estado
→ render: calcula a próxima árvore de UI
→ reconciliação: compara identidades e posições
→ commit: cria, atualiza ou remove nós DOM necessários
→ navegador: executa o pipeline visual aplicável

“Virtual DOM” é um nome comum para a representação em memória usada nesse processo, mas não torna toda aplicação automaticamente rápida. Renderizações caras, efeitos indevidos, árvores enormes e layout thrashing ainda custam. Vue e Angular têm estratégias próprias; todos continuam usando APIs e objetos da plataforma.

Modelo mental final

  • HTML é entrada serializada; DOM é o modelo vivo produzido e mantido pelo navegador.
  • A árvore contém diferentes tipos de nó; Element é apenas um deles.
  • APIs DOM são interfaces da plataforma Web expostas ao JavaScript.
  • Eventos percorrem um caminho com captura, alvo e, quando aplicável, bubbling.
  • Uma mutação DOM e uma atualização visual são conceitos relacionados, não sinônimos.
  • Desempenho depende sobretudo do trabalho provocado: estilo, layout, paint, composição e handlers.
  • Frameworks calculam e coordenam mudanças, mas no navegador o resultado final ainda chega ao DOM.

Fontes

EOF, Israel Santos

← voltar aos artigos