Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

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.

Rodolfo Mori8 min de leitura

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:

html
<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:

js
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);
está na página? false está na página? true itens agora : 4 <li class="item" data-id="4">Headset com microfone</li>

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:

js
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);
<li><span class="nome">Headset com microfone</span> — R$ 249,90</li> append devolve : undefined appendChild devolve: I

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:

js
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);
}
Brinde: mousepad Teclado mecânico Frete grátis daqui pra baixo Mouse sem fio Cupom PRIMEIRACOMPRA aplicado Cadeira gamer

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

js
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);
itens agora: 2 removeChild devolveu: Cadeira gamer ainda está na página? false recolocada: 2 true

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

js
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));
antes : 3 depois: 0 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:

js
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);
itens: 5 <li class="item"><span class="nome">Webcam Full HD</span> <span class="preco">329.00</span></li>

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:

js
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');
raso : <li class="item" data-id="1"></li> profundo: <li class="item" data-id="1"><span class="nome">Teclado mecânico</span> <span class="preco">289.90</span></li> cupom aplicado 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:

js
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;
  }
});
append direto : 0.3 ms - 500 itens DocumentFragment : 0.2 ms - 500 itens append + leitura de layout: 18.4 ms - 500 itens

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:

js
const lista = document.querySelector('.itens');

lista.appendChild('<li class="item">Headset com microfone</li>');
Uncaught TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'. at inserir-item.js:3:7

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:

js
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));
elementos: 3 último nó: #text "<li class=\"item\">Headset com microfone</li>"

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:

js
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);
<li class="item" data-id="4"><span class="nome">&lt;img src=x onerror="alert(1)"&gt;</span> <span class="preco">249.90</span></li> imagens na página: 0

O outerHTML mostra os sinais escapados como &amp;lt; e &amp;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.

Ver todos os vídeos do canal
  • dom
  • createelement
  • appendchild
  • documentfragment
  • clonenode

Perguntas frequentes

append ou appendChild?
append, salvo quando você precisa do valor de retorno. Ele aceita texto solto, aceita vários nós de uma vez e não devolve nada. appendChild aceita um nó só e devolve esse nó, o que ajuda em encadeamento.
DocumentFragment ainda vale a pena?
Vale quando você monta a estrutura em pedaços e quer uma única inserção, ou quando algo lê layout entre as inserções. Só pelo tempo de append num laço limpo, a diferença medida é irrelevante em navegador moderno.
O elemento removido com remove() é apagado da memória?
Não imediatamente. Se alguma variável ainda apontar para ele, o objeto continua vivo e pode ser reinserido depois. Só quando nada mais o referencia é que o coletor de lixo recolhe.
cloneNode copia os eventos que eu registrei?
Não. O clone copia tags, atributos e, com o argumento true, os descendentes. Ouvintes adicionados com addEventListener ficam de fora, e é por isso que o botão clonado não responde ao clique.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — Document: createElement() — developer.mozilla.org
  2. MDN — DocumentFragment — developer.mozilla.org

Continue por aqui