createElement: criar e remover elementos com JS
Como criar elementos com createElement, inserir com append e prepend, remover com remove e clonar um template — com a medição real do DocumentFragment.
document.createElement('li') cria um objeto HTMLElement que ainda está fora
da árvore DOM. Ele só passa a fazer parte da página quando você o insere com um
método como pai.append(elemento). elemento.remove() desfaz essa ligação com
a árvore.
Pense numa marcenaria ao lado de uma casa. createElement monta a peça na
bancada; propriedades, classes e texto são os ajustes; append instala a peça
num cômodo. Enquanto ela está na bancada, existe na memória, mas ninguém a vê na
página. No DOM, isConnected mostra justamente se o nó já está ligado ao
documento.
Esse caminho também é seguro para dados externos: em vez de entregar uma string
ao parser de HTML, você cria cada nó e coloca conteúdo textual por
textContent. Uma tag digitada por um cliente continua sendo texto, não uma
instrução para o navegador.
A página é o mesmo carrinho das lições anteriores, agora com um <template> no
fim para servir de molde:
<main id="carrinho">
<ul class="itens">
<li class="item" data-id="1"><span class="nome">Teclado mecânico</span> <span class="preco">289.90</span></li>
<li class="item" data-id="2"><span class="nome">Mouse sem fio</span> <span class="preco">149.90</span></li>
<li class="item" data-id="3"><span class="nome">Cadeira gamer</span> <span class="preco">1199.00</span></li>
</ul>
<button id="aplicar">Aplicar cupom</button>
<template id="modelo-item"><li class="item"><span class="nome"></span> <span class="preco"></span></li></template>
</main>O elemento nasce fora da página
createElement devolve um elemento solto na memória. Ele existe, você pode
configurá-lo à vontade, e ninguém vê nada até a inserção:
const lista = document.querySelector('.itens');
const li = document.createElement('li');
li.className = 'item';
li.dataset.id = '4';
li.textContent = 'Headset com microfone';
console.log('está na página?', li.isConnected);
lista.append(li);
console.log('está na página?', li.isConnected);
console.log('itens agora :', lista.children.length);
console.log(lista.lastElementChild.outerHTML);isConnected responde a pergunta exata: este nó faz parte do documento? Antes
do append, não. Depois, sim — e só a partir daí o navegador precisa calcular
layout e pintar.
Isso tem uma consequência prática boa: configurar o elemento antes de inserir é de graça. Definir dez propriedades num elemento solto não custa nenhum recálculo; fazer o mesmo num elemento que já está na tela pode custar dez.
Repare também no dataset.id: ele escreve o atributo data-id. Todo atributo
data-alguma-coisa aparece em dataset com o nome em camelCase, e é a forma
recomendada de pendurar dado num elemento.
append e appendChild não são a mesma coisa
Os dois inserem no fim, mas têm assinaturas diferentes:
const li = document.createElement('li');
const nome = document.createElement('span');
nome.className = 'nome';
nome.textContent = 'Headset com microfone';
li.append(nome, ' — ', 'R$ 249,90');
console.log(li.outerHTML);
const daAppend = li.append(document.createElement('b'));
const daAppendChild = li.appendChild(document.createElement('i'));
console.log('append devolve :', daAppend);
console.log('appendChild devolve:', daAppendChild.tagName);Numa chamada só, append recebeu um elemento e duas strings. As strings viraram
nós de texto — e, como são texto, qualquer sinal de menor dentro delas seria
escapado, não interpretado.
append |
appendChild |
|
|---|---|---|
| aceita string como texto | sim | não, lança TypeError |
| aceita vários argumentos | sim | um só |
| devolve | undefined |
o nó inserido |
existe em DocumentFragment |
sim | sim |
Na prática: use append. appendChild continua no código de todo mundo porque
é vinte anos mais velho, e o valor de retorno às vezes é conveniente — mas o
TypeError dele é a armadilha do fim desta lição.
Onde inserir: prepend, before, after
append põe no fim. Os outros três resolvem as demais posições, e before e
after são chamados no irmão, não no pai:
const lista = document.querySelector('.itens');
const mouse = lista.querySelector('[data-id="2"]');
const brinde = document.createElement('li');
brinde.textContent = 'Brinde: mousepad';
lista.prepend(brinde);
const aviso = document.createElement('li');
aviso.textContent = 'Frete grátis daqui pra baixo';
mouse.before(aviso);
const cupom = document.createElement('li');
cupom.textContent = 'Cupom PRIMEIRACOMPRA aplicado';
mouse.after(cupom);
for (const filho of lista.children) {
console.log(filho.querySelector('.nome')?.textContent ?? filho.textContent);
}Você vai encontrar pai.insertBefore(novo, referencia) em código mais antigo.
Ele faz o mesmo que referencia.before(novo), com a ordem dos argumentos
invertida e o pai como intermediário — uma fonte generosa de confusão. A versão
nova é mais curta e lê na ordem em que você pensa.
Remover, e o que sobra na memória
const lista = document.querySelector('.itens');
lista.querySelector('[data-id="2"]').remove();
console.log('itens agora:', lista.children.length);
const cadeira = lista.querySelector('[data-id="3"]');
const removida = lista.removeChild(cadeira);
console.log('removeChild devolveu:', removida.querySelector('.nome').textContent);
console.log('ainda está na página?', removida.isConnected);
lista.append(removida);
console.log('recolocada:', lista.children.length, removida.isConnected);Duas lições nessa saída. A primeira: remove() é chamado no próprio elemento e
não precisa do pai — é o que você quer usar. A segunda, mais interessante:
tirar da página não apaga o objeto. A variável removida continua apontando
para um elemento completo, com os filhos e o texto, apenas desconectado. Basta
inserir de novo para ele voltar exatamente como estava, com listeners e tudo.
Só quando nenhuma variável mais aponta para ele é que o coletor de lixo recolhe. Guardar referência de elemento removido em array é uma das formas silenciosas de vazar memória numa aplicação de página única.
Esvaziar uma lista
const lista = document.querySelector('.itens');
console.log('antes :', lista.children.length);
lista.replaceChildren();
console.log('depois:', lista.children.length);
console.log('innerHTML:', JSON.stringify(lista.innerHTML));replaceChildren() sem argumento nenhum limpa tudo. Com argumentos, substitui o
conteúdo pelo que você passar — mesma flexibilidade do append. É a alternativa
direta ao innerHTML = '', que faz o navegador parsear uma string vazia para
chegar ao mesmo lugar.
<template> e cloneNode: o molde do item
Montar cada <span> na mão fica cansativo depois do segundo campo. A tag
<template> guarda um pedaço de HTML que o navegador parseia mas não
renderiza — um molde pronto para clonar:
const modelo = document.getElementById('modelo-item');
const lista = document.querySelector('.itens');
const novos = [
{ nome: 'Headset com microfone', preco: '249.90' },
{ nome: 'Webcam Full HD', preco: '329.00' },
];
const pedaco = document.createDocumentFragment();
for (const produto of novos) {
const copia = modelo.content.firstElementChild.cloneNode(true);
copia.querySelector('.nome').textContent = produto.nome;
copia.querySelector('.preco').textContent = produto.preco;
pedaco.append(copia);
}
lista.append(pedaco);
console.log('itens:', lista.children.length);
console.log(lista.lastElementChild.outerHTML);O conteúdo do template mora em modelo.content, que é um DocumentFragment — e
por isso não aparece na página nem na busca por .item. Clonamos o molde,
preenchemos os textos e inserimos a cópia. A estrutura fica no HTML, onde é
fácil de ler; o JavaScript só preenche.
Clone raso, clone profundo, e o que não vem junto
cloneNode() sem argumento copia só o elemento. Com true, copia a subárvore
inteira:
const item = document.querySelector('[data-id="1"]');
console.log('raso :', item.cloneNode().outerHTML);
console.log('profundo:', item.cloneNode(true).outerHTML);
const botao = document.getElementById('aplicar');
botao.addEventListener('click', () => console.log('cupom aplicado'));
botao.click();
const copia = botao.cloneNode(true);
document.getElementById('carrinho').append(copia);
copia.click();
console.log('fim');O clone raso trouxe as classes e o data-id, mas nenhum filho. O profundo veio
completo.
E o botão clonado não respondeu ao clique. Nenhum “cupom aplicado” apareceu
depois do copia.click() — só o fim. Atributos são copiados; ouvintes
registrados com addEventListener, não. É por isso que clonar um formulário
inteiro “quase funciona” e depois nada responde.
DocumentFragment e a medição que desmente o mito
Toda apostila diz que inserir num DocumentFragment antes de mandar para a
página evita reflows e é muito mais rápido. Medi as três variações com 500 itens
no Chromium 151:
const lista = document.querySelector('.itens');
const produtos = Array.from({ length: 500 }, (_, i) => 'Produto ' + (i + 1));
function medir(rotulo, montar) {
lista.replaceChildren();
const inicio = performance.now();
montar();
const fim = performance.now();
console.log(rotulo, (fim - inicio).toFixed(1) + ' ms', '-', lista.children.length, 'itens');
}
medir('append direto :', () => {
for (const produto of produtos) {
const li = document.createElement('li');
li.textContent = produto;
lista.append(li);
}
});
medir('DocumentFragment :', () => {
const pedaco = document.createDocumentFragment();
for (const produto of produtos) {
const li = document.createElement('li');
li.textContent = produto;
pedaco.append(li);
}
lista.append(pedaco);
});
medir('append + leitura de layout:', () => {
for (const produto of produtos) {
const li = document.createElement('li');
li.textContent = produto;
lista.append(li);
li.offsetHeight;
}
});As duas primeiras deram praticamente o mesmo tempo. O navegador moderno não
recalcula layout a cada append: ele agenda o recálculo para antes do próximo
quadro e faz tudo de uma vez. Inserir 500 elementos num laço limpo já é barato.
A terceira linha é o que realmente importa. A única diferença dela é
li.offsetHeight — uma leitura de layout dentro do laço. Sessenta vezes mais
lento. Ler offsetHeight, getBoundingClientRect, offsetWidth, scrollTop
ou getComputedStyle obriga o navegador a calcular o layout naquele
instante, porque você pediu um número que só existe depois do cálculo. Como
você acabou de escrever no DOM, ele precisa refazer tudo. E aí de novo, 500
vezes.
Isso tem nome: layout thrashing. O DocumentFragment protege contra ele —
enquanto os elementos estão no fragmento, fora do documento, não há layout para
recalcular. Mas o mérito não é “evitar reflow ao inserir”: é evitar reflow
quando alguma coisa lê layout no meio do caminho.
O erro: parameter 1 is not of type 'Node'
appendChild aceita nó, e só nó. Passar uma string de HTML — que é o reflexo de
quem vinha usando innerHTML — lança:
const lista = document.querySelector('.itens');
lista.appendChild('<li class="item">Headset com microfone</li>');A mensagem é literal: o argumento 1 não é do tipo Node. Você passou uma
string, e o método espera um objeto da árvore.
O mais traiçoeiro é o que acontece se você “corrigir” trocando para append,
que aceita string:
const lista = document.querySelector('.itens');
lista.append('<li class="item">Headset com microfone</li>');
console.log('elementos:', lista.children.length);
console.log('último nó:', lista.lastChild.nodeName, JSON.stringify(lista.lastChild.nodeValue));Nenhum erro, e nenhum item novo. A string virou um nó de texto, e o usuário
vê a tag escrita na tela, com os sinais de menor e maior. append trata string
como texto por definição — o que é exatamente a proteção contra XSS de que a
lição sobre innerHTML fala.
Se você tem uma string de HTML e quer que ela vire elemento, o método é
insertAdjacentHTML('beforeend', html) — e aí valem todas as ressalvas de
segurança.
Montando um item com dado do cliente
Fechando com o caso real que justifica esta lição inteira. Um produto cujo nome foi cadastrado com uma tag dentro:
const lista = document.querySelector('.itens');
function montarItem(produto) {
const li = document.createElement('li');
li.className = 'item';
li.dataset.id = produto.id;
const nome = document.createElement('span');
nome.className = 'nome';
nome.textContent = produto.nome;
const preco = document.createElement('span');
preco.className = 'preco';
preco.textContent = produto.preco.toFixed(2);
li.append(nome, ' ', preco);
return li;
}
lista.append(montarItem({ id: 4, nome: '<img src=x onerror="alert(1)">', preco: 249.9 }));
console.log(lista.lastElementChild.outerHTML);
console.log('imagens na página:', document.images.length);O outerHTML mostra os sinais escapados como &lt; e &gt;, e a
contagem de imagens é zero: nenhuma tag foi criada. O nome aparece na tela
exatamente como foi cadastrado, feio e inofensivo.
Compare com a versão em uma linha que todo mundo escreve primeiro —
lista.innerHTML += '<li>' + produto.nome + '</li>' — e você tem, de uma vez
só, o XSS, os listeners perdidos e o custo quadrático. Trocar por uma função de
vinte linhas parece retrocesso; é o contrário.
Onde isso te deixa
Você já sabe achar um elemento, ler e escrever o conteúdo dele, mudar a aparência e criar estrutura nova do zero. Falta a peça que liga tudo à pessoa do outro lado: eventos — clique, digitação, envio de formulário. É o próximo passo da trilha de JavaScript.
Enquanto isso, um exercício que usa as quatro lições de DOM juntas: pegue um
array de cinco produtos, monte a lista inteira com createElement, dê a cada
item um botão de remover, e faça o total no rodapé se atualizar a cada remoção.
Se algo devolver null pelo caminho, a explicação está em
selecionar elementos; se
você não souber por onde começar, o
guia completo de JavaScript mostra a ordem inteira.
Prefere aprender em vídeo?
Tem uma aula sobre este assunto no nosso canal.
Perguntas frequentes
append ou appendChild?
DocumentFragment ainda vale a pena?
O elemento removido com remove() é apagado da memória?
cloneNode copia os eventos que eu registrei?
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 Chromium 151.0.7922.34, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Document: createElement() — developer.mozilla.org
- MDN — DocumentFragment — developer.mozilla.org



