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.
<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.
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.
EventTarget
└── Node
└── Element
└── HTMLElement
└── HTMLButtonElementO 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.
| API | Retorno | Inclui texto/comentários? | Coleção |
|---|---|---|---|
| element.children | Elementos filhos | Não | HTMLCollection viva |
| node.childNodes | Todos os nós filhos | Sim | NodeList viva |
| querySelectorAll() | Elementos encontrados | Não | NodeList estática |
| getElementsByClassName() | Elementos encontrados | Não | HTMLCollection 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
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.
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
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
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 conectadoUm 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.
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.
<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.
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
}Captura: Window → Document → html → body → button
Alvo: button
Bubbling: Window ← Document ← html ← body ← buttonstopPropagation 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
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.
| Etapa | Responsabilidade |
|---|---|
| Style | Descobrir quais regras CSS se aplicam |
| Layout | Calcular tamanhos e posições geométricas |
| Paint | Desenhar texto, cores, bordas e sombras em camadas |
| Composite | Combinar 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
// 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
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.
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.
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
- ▸WHATWG, DOM Standard: https://dom.spec.whatwg.org/
- ▸WHATWG, Parsing de documentos HTML: https://html.spec.whatwg.org/multipage/parsing.html
- ▸MDN, Document Object Model (DOM): https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model
- ▸MDN, Anatomia do DOM: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Anatomy_of_the_DOM
- ▸MDN, Event bubbling e delegação: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling
- ▸web.dev, Rendering performance: https://web.dev/articles/rendering-performance
- ▸web.dev, Layout thrashing: https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing
- ▸React, Render and Commit: https://react.dev/learn/render-and-commit
✓ EOF, Israel Santos
← voltar aos artigos