Difficulty: Intermediate
The Document Object Model (DOM) is a tree-structured representation of an HTML document that the browser creates when it parses your HTML. Every element, attribute, and piece of text becomes a node in this tree. JavaScript can access and manipulate this tree through the document object, allowing you to dynamically change the content, structure, and styling of a web page without reloading it.
Selecting elements is the first step in any DOM operation. The modern API provides two primary methods: document.querySelector(selector) returns the first element matching a CSS selector, and document.querySelectorAll(selector) returns a static NodeList of all matches. Older methods like getElementById, getElementsByClassName, and getElementsByTagName still work and can be slightly faster for simple lookups, but querySelector's CSS selector syntax is more flexible and consistent. Note that querySelectorAll returns a static snapshot while getElementsBy* methods return live collections that update automatically.
Creating and inserting elements involves document.createElement(tag) to create a new element, setting its properties (textContent, innerHTML, setAttribute, classList), and then inserting it into the DOM. Traditional insertion methods include appendChild (adds as last child) and insertBefore (inserts before a reference node). Modern methods - append, prepend, after, before - accept multiple arguments and text strings directly, making them more convenient. The replaceWith method replaces an element entirely.
Removing elements is straightforward: element.remove() removes the element from the DOM (modern), while parentNode.removeChild(child) is the older approach. When removing elements, be mindful of event listeners attached to them - listeners on removed elements are garbage collected if no other references exist, but delegated listeners on parent elements persist.
For batch operations, DocumentFragment is essential. A DocumentFragment is a lightweight, off-DOM container. You can append multiple elements to it without triggering reflows, then insert the entire fragment into the DOM in a single operation. This is significantly more performant than inserting elements one by one, as each individual DOM insertion can trigger the browser's layout engine.
Performance is critical when manipulating the DOM. Every time you modify the DOM, the browser may need to recalculate styles, compute layout (reflow), and repaint pixels on screen. Reading layout properties (offsetWidth, getBoundingClientRect) after writing to the DOM forces a synchronous reflow. The best practice is to batch all reads together and all writes together, minimize DOM access by caching references in variables, use DocumentFragment or innerHTML for bulk insertions, and prefer classList over direct style manipulation.
// querySelector - returns first match
const header = document.querySelector('h1');
const navLinks = document.querySelectorAll('nav a');
// Older methods (still useful)
const byId = document.getElementById('app');
const byClass = document.getElementsByClassName('card');
// querySelectorAll returns a static NodeList
// Convert to array for full array methods
const linksArray = Array.from(navLinks);
const hrefs = linksArray.map(link => link.href);
// Traversal from an element
const list = document.querySelector('ul');
console.log(list.children); // HTMLCollection of child elements
console.log(list.firstElementChild); // first child element
console.log(list.parentElement); // parent element
console.log(list.nextElementSibling); // next sibling element
querySelector uses CSS selector syntax, making it versatile for any selection pattern. querySelectorAll returns a static NodeList that does not update when the DOM changes, unlike getElementsBy* which return live collections.
// Create a new card element
const card = document.createElement('div');
card.classList.add('card', 'shadow-md');
card.setAttribute('data-id', '42');
const title = document.createElement('h3');
title.textContent = 'New Card Title';
const body = document.createElement('p');
body.textContent = 'Card description goes here.';
// Build the structure
card.appendChild(title);
card.appendChild(body);
// Modern insertion methods
const container = document.querySelector('#container');
container.append(card); // add as last child
// container.prepend(card); // add as first child
// container.before(card); // add before container
// container.after(card); // add after container
// Modifying existing elements
const heading = document.querySelector('h1');
heading.textContent = 'Updated Title'; // safe, text only
heading.classList.toggle('active'); // toggle a class
heading.style.color = 'blue'; // inline style
heading.setAttribute('aria-label', 'Main heading');
createElement creates an element in memory. You configure it with properties and classes, then insert it into the DOM. The modern append/prepend/before/after methods are more flexible than appendChild/insertBefore.
// Without fragment - triggers reflow on each iteration
// BAD: const list = document.querySelector('ul');
// items.forEach(item => {
// const li = document.createElement('li');
// li.textContent = item;
// list.appendChild(li); // reflow each time!
// });
// With DocumentFragment - single reflow
const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
li.classList.add('list-item');
fragment.appendChild(li); // no reflow - fragment is off-DOM
});
const list = document.querySelector('ul');
list.appendChild(fragment); // single reflow for all 5 items
console.log(list.children.length); // 5
DocumentFragment acts as an invisible container. Elements are added to it without triggering layout recalculations. When the fragment is appended to the DOM, all its children are moved in one operation, causing only a single reflow.
// Modern removal
const obsolete = document.querySelector('.old-banner');
if (obsolete) {
obsolete.remove(); // removes from DOM
}
// Removing all children
const container = document.querySelector('#list');
// Method 1: innerHTML (fast but destroys event listeners)
container.innerHTML = '';
// Method 2: replaceChildren (modern, clean)
container.replaceChildren(); // removes all children
// Method 3: loop removal (preserves control)
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// innerHTML for bulk creation (when you trust the content)
container.innerHTML = `
<li class="item">First</li>
<li class="item">Second</li>
<li class="item">Third</li>
`;
console.log(container.children.length);
element.remove() is the modern way to remove an element. For clearing children, replaceChildren() is the cleanest approach. innerHTML is fast for bulk HTML insertion but should never be used with untrusted content (XSS risk).
DOM, Selectors, createElement, classList, appendChild, DocumentFragment, Reflow, Repaint