Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

DocumentFragment, insertAdjacentHTML, replaceChildren: Fast DOM in 2026

C
Carlo Fontanos
· 2 min read

The innocent-looking line that tanks list rendering:

for (const item of items) {
    list.innerHTML += renderRow(item);   // ☠
}

innerHTML += means: serialize the entire existing list to a string, append one row, re-parse everything, rebuild every node. For 500 items that's 500 full rebuilds - quadratic work - and every rebuild destroys existing nodes, killing their event listeners, form state and CSS transitions. The "why did my checkboxes uncheck" bug and the "why is this slow" bug are the same bug.

Tool 1: build once, insert once

// String route - fine when data is trusted/escaped
list.insertAdjacentHTML('beforeend', items.map(renderRow).join(''));

// Node route - when you're creating elements programmatically
const fragment = document.createDocumentFragment();
for (const item of items) fragment.append(buildRow(item));
list.append(fragment);   // ONE insertion, one layout pass

A DocumentFragment is a weightless container: appending to it touches no live DOM, and inserting it moves its children in a single operation. Either route turns 500 mutations into one.

Tool 2: insertAdjacentHTML's four positions

el.insertAdjacentHTML('beforebegin', html);   // sibling before el
el.insertAdjacentHTML('afterbegin', html);    // first child
el.insertAdjacentHTML('beforeend', html);     // last child (the += replacement)
el.insertAdjacentHTML('afterend', html);      // sibling after el

Parses and inserts without touching existing children - listeners and state survive. This is the one-line drop-in fix for legacy innerHTML += code, and unlike innerHTML it can insert siblings, not just children. (Escape untrusted data first - an auto-escaping html tag makes that structural.)

Tool 3: replaceChildren for the swap-everything case

list.replaceChildren(...newRows);     // clear + insert, one call
list.replaceChildren();               // just clear (faster & cleaner than innerHTML = '')

Filtering, re-sorting, new search results - the "throw away the old view" update. It accepts nodes and fragments, does the teardown and insertion atomically, and reads as exactly what it does.

The template bonus

For repeated structures, parse the markup once in a <template> and stamp clones - roughly the fastest structured-DOM creation short of manual createElement trees:

const rowTpl = document.querySelector('#row-template');
const row = rowTpl.content.cloneNode(true);
row.querySelector('.name').textContent = item.name;   // textContent: no escaping worries
fragment.append(row);

Rules of thumb: batch into one insertion, prefer textContent for data slots, and if a list re-renders constantly with small changes, that's the problem virtual-DOM libraries and keyed diffing exist for - know when you've outgrown hand-rolled updates. Up to that point, these three tools plus a fragment are embarrassingly fast.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading