The DOM (Document Object Model) is the browser’s in-memory representation of an HTML page as a tree of objects. JavaScript can modify this tree to add, update, and remove elements — this is what makes web pages interactive. Understanding the DOM manipulation APIs well means knowing not just the methods, but when each approach is appropriate and how to avoid common performance pitfalls.
Creating Elements
The cleanest way to create elements programmatically is document.createElement, which gives you a proper DOM element you can configure before inserting:
// Create an element, configure it, then insert it
const card = document.createElement('div');
card.className = 'user-card';
card.dataset.userId = '42'; // sets data-user-id="42"
const name = document.createElement('h3');
name.textContent = 'Alice'; // textContent is safe — no HTML parsing
const email = document.createElement('p');
email.textContent = '[email protected]';
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Edit';
button.setAttribute('aria-label', 'Edit Alice');
card.append(name, email, button); // append multiple children in one call
document.getElementById('userList').append(card);
textContent is always safe to use — it sets literal text. innerHTML parses the string as HTML, which is efficient for templates but dangerous with user-supplied data (it creates XSS vulnerabilities).
innerHTML: When to Use It
innerHTML is convenient for inserting template-driven content at once, and it’s faster than building elements with createElement when inserting many nodes. Use it for static templates with no user-supplied data:
// ✅ Safe — static template with no user data interpolated
function renderEmptyState(message) {
return `
<div class="empty-state">
<svg>...</svg>
<h3>Nothing here yet</h3>
<p>${message}</p>
</div>
`;
}
container.innerHTML = renderEmptyState('Add your first item to get started.');
// ❌ Dangerous — user data in innerHTML creates XSS risk
const userComment = getCommentFromDB(); // could contain <script>alert(1)</script>
container.innerHTML = `<p>${userComment}</p>`; // NEVER do this
// ✅ Safe way to mix template with user data — set textContent after inserting
const p = document.createElement('p');
container.innerHTML = '<p></p>';
container.querySelector('p').textContent = userComment; // safe
Inserting Elements
Modern DOM APIs give you several insertion points:
const parent = document.getElementById('list');
const newItem = document.createElement('li');
newItem.textContent = 'New item';
const firstItem = parent.firstElementChild;
parent.append(newItem); // insert as last child
parent.prepend(newItem); // insert as first child
parent.insertBefore(newItem, firstItem); // insert before a reference node
firstItem.before(newItem); // insert before firstItem (same result)
firstItem.after(newItem); // insert after firstItem
// insertAdjacentHTML — efficient for inserting HTML relative to an element
firstItem.insertAdjacentHTML('beforebegin', '<li>Before first</li>'); // before element
firstItem.insertAdjacentHTML('afterbegin', '<li>First child</li>'); // first child
firstItem.insertAdjacentHTML('beforeend', '<li>Last child</li>'); // last child
firstItem.insertAdjacentHTML('afterend', '<li>After first</li>'); // after element
append and prepend accept both strings and DOM nodes; appendChild only accepts nodes. For modern code, append/prepend/before/after cover most needs clearly.
Updating Elements
Updating content and attributes follows a consistent pattern — read with a getter, write with a setter or method:
const element = document.querySelector('.status-badge');
// Text content
element.textContent = 'Active'; // replace text
console.log(element.textContent); // read text
// Attributes
element.setAttribute('aria-label', 'Status: Active');
element.getAttribute('aria-label'); // 'Status: Active'
element.removeAttribute('disabled');
element.hasAttribute('disabled'); // false
// CSS classes — classList is the preferred API
element.classList.add('active', 'visible');
element.classList.remove('inactive');
element.classList.toggle('highlighted'); // add if absent, remove if present
element.classList.replace('loading', 'loaded');
element.classList.contains('active'); // true
// Inline styles (prefer CSS classes for styling; use style for dynamic values)
element.style.transform = `translateX(${offsetPx}px)`;
element.style.opacity = '0.5';
// Dataset (data-* attributes)
element.dataset.status = 'active'; // sets data-status="active"
console.log(element.dataset.status); // 'active'
Prefer CSS classes over inline styles whenever possible — it keeps presentation in CSS where it belongs. Use style only for truly dynamic values like positions calculated in JavaScript.
Removing Elements
// Remove an element directly (modern API)
document.getElementById('toast').remove();
// Remove a child from its parent (older API, still useful when you have the parent)
const list = document.getElementById('list');
const first = list.firstElementChild;
list.removeChild(first);
// Clear all children — innerHTML is fast but loses event listeners
list.innerHTML = '';
// Safer alternative — replaceChildren() removes all children, accepts new ones
list.replaceChildren(); // clear
list.replaceChildren(newItem1, newItem2); // replace with new children
If the elements being removed have event listeners, remove those listeners first (or use event delegation so they don’t need individual listeners).
Cloning Elements
cloneNode(true) deep-copies an element with all its children. cloneNode(false) copies just the element, not its children:
const template = document.getElementById('cardTemplate');
function createCard(user) {
const card = template.cloneNode(true); // deep clone
card.removeAttribute('id'); // avoid duplicate IDs
card.querySelector('.card-name').textContent = user.name;
card.querySelector('.card-email').textContent = user.email;
card.querySelector('.card-btn').addEventListener('click', () => openUser(user.id));
return card;
}
Cloning is more efficient than rebuilding the element from scratch when the template structure is complex.
Performance: DocumentFragment for Batch Inserts
Every time you insert an element directly into the live DOM, the browser may trigger a reflow — recalculating layout for the affected area. Inserting 1000 items one by one triggers up to 1000 reflows.
DocumentFragment is an off-screen container. You build the entire structure in the fragment, then insert it once — causing a single reflow:
// ❌ 1000 reflows — one per appendChild
const list = document.getElementById('results');
for (const item of largeDataset) {
const li = document.createElement('li');
li.textContent = item.name;
list.appendChild(li); // touches the live DOM each time
}
// ✅ 1 reflow — fragment is not in the live DOM until the final insert
const list = document.getElementById('results');
const fragment = document.createDocumentFragment();
for (const item of largeDataset) {
const li = document.createElement('li');
li.textContent = item.name;
fragment.appendChild(li); // no reflow
}
list.appendChild(fragment); // single DOM update, single reflow
For very large lists (hundreds to thousands of items), consider virtual rendering instead — only create DOM elements for the items currently visible in the viewport.
Building Dynamic UI Components
Putting it together — a rendering function that builds a component from data without touching the live DOM until it’s ready:
function renderUserList(users) {
const fragment = document.createDocumentFragment();
users.forEach(user => {
const li = document.createElement('li');
li.className = `user-item ${user.active ? 'active' : 'inactive'}`;
li.dataset.id = user.id;
const avatar = document.createElement('img');
avatar.src = user.avatarUrl;
avatar.alt = `${user.name}'s avatar`;
avatar.className = 'user-avatar';
const info = document.createElement('div');
info.className = 'user-info';
info.innerHTML = `
<strong class="user-name"></strong>
<span class="user-email"></span>
`;
// Set user-supplied text safely via textContent
info.querySelector('.user-name').textContent = user.name;
info.querySelector('.user-email').textContent = user.email;
li.append(avatar, info);
fragment.append(li);
});
const list = document.getElementById('userList');
list.replaceChildren(fragment); // clear and replace in one step
}
Note the pattern: innerHTML sets the structural template (safe, static HTML), then textContent sets the user-supplied data (safe, no parsing). This combines the efficiency of innerHTML with the XSS safety of textContent.
Summary
The key principles for DOM manipulation:
- Use
createElementwhen building elements that need event listeners or user data - Use
innerHTMLfor static templates; never interpolate user-supplied strings into it - Batch DOM insertions with
DocumentFragment— one insert, one reflow classListis the right API for CSS class changes; avoidelement.className = '...'- Remove event listeners before removing elements to avoid memory leaks
- Cache DOM queries —
getElementByIdinside a loop is expensive
Comments