Templates, cloning and efficient rendering
Use the template element for inert markup, clone it into a fragment, and render lists without rebuilding the whole DOM on every change.
The template element
<template id="row-template">
<li class="row">
<img class="row__avatar" alt="" width="32" height="32">
<span class="row__name"></span>
<button class="row__remove" type="button">Remove</button>
</li>
</template>const tpl = document.querySelector('#row-template');
tpl.content; // a DocumentFragment - the real markup store
tpl.content.childNodes.length; // 1
// clone, fill, insert
const node = tpl.content.cloneNode(true); // deep
const li = node.querySelector('.row');
li.dataset.id = user.id;
li.querySelector('.row__name').textContent = user.name;
li.querySelector('.row__avatar').src = user.avatar;
list.append(node);- A template's content lives in a separate document fragment: it is parsed, but not rendered, and its images are not fetched.
- Scripts inside a template do not run, and styles inside it do not apply until it is cloned into the document.
contentis a fragment, so append the result ofcloneNode, nevercontentitself - appending it moves the nodes out of the template and it can only be used once.cloneNode(true)copies the subtree; the default shallow clone returns an empty element.
⚠️
Cloning
tpl.content instead of a copy of it consumes the template. The first render works and every later render produces nothing, which looks like a rendering bug rather than a mistake about node ownership.Batching with a fragment
function render(users) {
const tpl = document.querySelector('#row-template');
const fragment = document.createDocumentFragment();
for (const user of users) {
const node = tpl.content.cloneNode(true);
node.querySelector('.row').dataset.id = user.id;
node.querySelector('.row__name').textContent = user.name;
fragment.append(node); // no layout work yet
}
// one mutation, one reflow
list.replaceChildren(fragment);
}// the expensive alternative: one insert per row, each triggering work
for (const user of users) {
list.append(makeRow(user)); // 500 inserts, 500 layout invalidations
}
// and the worst version: rebuilding innerHTML in a loop
list.innerHTML = '';
users.forEach((u) => { list.innerHTML += makeRowHtml(u); }); // reparses everything| Approach | Mutations | Note |
|---|---|---|
innerHTML += in a loop | n | Reparses the whole subtree each time |
append per node | n | Layout work per insert |
Fragment then one replaceChildren | 1 | The default for list rendering |
insertAdjacentHTML once | 1 | Fast, but parses HTML you must escape |
Updating without a full rebuild
// keyed updates: keep existing nodes, change only what differs
function update(users) {
const byId = new Map(
[...list.children].map((el) => [el.dataset.id, el])
);
for (const user of users) {
let el = byId.get(String(user.id));
if (!el) {
el = buildRow(user);
list.append(el);
} else {
el.querySelector('.row__name').textContent = user.name;
byId.delete(String(user.id)); // handled
}
}
// whatever is left in the map is no longer in the data
for (const el of byId.values()) el.remove();
}- Key rows by a stable id, never by index - indices change when an item is removed and produce confusing updates.
- Change text and attributes in place; only create a node when there is no existing one.
- Delete by difference rather than by clearing the container, so the scroll position and focus survive.
- If a full rebuild is simpler and the list is short, do the full rebuild. Clarity beats a reconciliation you cannot debug.
- Once the update logic has three special cases, that is the signal to reach for a framework rather than to add a fourth.
// preserving focus across a re-render
const activeId = document.activeElement?.dataset?.id;
update(users);
if (activeId) {
list.querySelector('[data-id="' + activeId + '"] .row__name')?.focus();
}FAQ
Is template faster than innerHTML?
Yes, and more importantly it is safer. The template is parsed once, and each render is a clone rather than a parse of an HTML string - so there is no interpolation step where user data could be read as markup. Use innerHTML only for markup you fully control.
Do I need a framework for list rendering?
Not for lists that are short, replaced wholesale or updated rarely. Keyed reconciliation by hand is worth it when the list is long or the user's scroll and focus must survive updates; beyond that, a framework earns its weight.
Related
Observing changes with MutationObserver Performance and memory in DOM code
Last refreshed 2026-09-18.