AlgoInsight

Free lesson · JavaScript Basic to Advance

DOM Mutation: Creating, Updating, Removing

Easy5 min read

Modifying the DOM

Selecting and reading is one half of DOM work. The other half is mutation — creating new elements, modifying existing ones, and removing elements that are no longer needed. Every interactive web application is built on these operations.

// Create a new element
const newDiv = document.createElement("div");
newDiv.textContent = "New content";
newDiv.className = "card";

// Insert it into the DOM
document.body.appendChild(newDiv);

// Modify an existing element
const existing = document.getElementById("title");
existing.textContent = "Updated title";

// Remove an element
const old = document.querySelector(".outdated");
old.remove();

These three operations — create, modify, remove — handle every DOM change. This Topic covers them in detail, including the performance considerations that matter at scale.

Creating Elements

The basic pattern: create an element, configure it, then insert it into the DOM.

// Create a basic element
const el = document.createElement("div");

// Configure it before inserting
el.id = "my-element";
el.className = "card highlighted";
el.textContent = "Hello world";
el.setAttribute("data-priority", "high");

// Now insert it
document.body.appendChild(el);

You can also create text nodes and attribute nodes, but in practice, almost all DOM creation goes through createElement and the element properties.

For creating multiple related elements, build them in memory first, then insert once:

const list = document.createElement("ul");

["Apple", "Banana", "Cherry"].forEach(fruit => {
  const item = document.createElement("li");
  item.textContent = fruit;
  list.appendChild(item);
});

// One DOM mutation at the end
document.body.appendChild(list);

Building in memory and inserting once is faster than inserting each element individually. We'll cover why under "DOM Performance" below.

Inserting Elements

Several methods for inserting, each with slightly different positioning:

parent.appendChild(newNode) — adds at the end of parent's children.

parent.prepend(newNode) — adds at the beginning.

parent.insertBefore(newNode, referenceNode) — adds before a specific existing child.

element.insertAdjacentElement(position, newElement) — flexible positioning relative to element:

const target = document.getElementById("target");
const newEl = document.createElement("div");

target.insertAdjacentElement("beforebegin", newEl);   // sibling before target
target.insertAdjacentElement("afterbegin", newEl);    // first child of target
target.insertAdjacentElement("beforeend", newEl);     // last child of target
target.insertAdjacentElement("afterend", newEl);      // sibling after target

The four positions cover every common insertion case. Modern code mostly uses appendChild/prepend for the simple cases and insertAdjacentElement for the others.

There's also insertAdjacentHTML(position, htmlString) which parses a string of HTML and inserts the result. Convenient but has the XSS risk of innerHTML. Use only with trusted content.

graph TD
    A[Reference element] --> B[beforebegin<br/>sibling before]
    A --> C[afterbegin<br/>first child inside]
    A --> D[beforeend<br/>last child inside]
    A --> E[afterend<br/>sibling after]

    style A fill:#7c3aed,color:#fff
This diagram shows the four insertion positions for insertAdjacentElement. beforebegin inserts a sibling immediately before the reference element. afterbegin inserts inside the reference element as its first child. beforeend inserts inside the reference element as its last child. afterend inserts a sibling immediately after the reference element. These four positions cover every common insertion case.

Modifying Existing Elements

For elements already in the DOM, you modify them through the same properties used to read them in Topic 12.2.

const el = document.getElementById("example");

// Text and HTML
el.textContent = "Updated";
el.innerHTML = "<strong>Bold</strong> text";   // careful with untrusted content

// Attributes
el.id = "new-id";
el.setAttribute("data-status", "active");
el.removeAttribute("data-status");

// Classes
el.className = "card highlighted";
el.classList.add("new-class");
el.classList.remove("old-class");
el.classList.toggle("active");

// Inline styles
el.style.backgroundColor = "lightblue";
el.style.fontSize = "16px";
el.style.display = "none";

// Or set multiple styles at once
Object.assign(el.style, {
  backgroundColor: "lightblue",
  fontSize: "16px",
  padding: "10px"
});

For style changes, classList toggle is usually preferred over direct style manipulation — keeping styles in CSS makes them easier to maintain. Use style for genuinely dynamic values (computed positions, animated values) where CSS rules don't fit.

Interactive animation: Classes over inline styles - flip state with a class, and write style only for computed values.

Removing Elements

Two ways to remove an element:

Modern (preferred):

const el = document.getElementById("outdated");
el.remove();   // removes from the DOM entirely

Older (still common):

const el = document.getElementById("outdated");
el.parentNode.removeChild(el);   // remove via the parent

element.remove() is simpler and works in all modern browsers. The older pattern was the only option before remove() was added.

For removing all children of an element:

const container = document.getElementById("container");

// Modern: replace children with nothing
container.replaceChildren();

// Or: remove children one at a time
while (container.firstChild) {
  container.firstChild.remove();
}

// Or: clear via innerHTML (works but parses an empty string)
container.innerHTML = "";

replaceChildren() with no arguments is the modern idiomatic way. The loop is the old idiom you'll still see in existing code.

DocumentFragment for Batch Insertions

When inserting many elements at once, DocumentFragment lets you build a subtree in memory and insert it as a single operation.

const fragment = document.createDocumentFragment();

for (let i = 0; i < 1000; i++) {
  const li = document.createElement("li");
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}

// One DOM mutation for 1000 elements
document.getElementById("list").appendChild(fragment);

When the fragment is appended to the DOM, all its children move into the target. The fragment itself disappears from the tree. The result: one layout/paint trigger instead of 1000.

For applications generating many DOM elements (lists, tables, grids), DocumentFragment is a real performance win. Modern frameworks handle this automatically internally.

DOM Performance

DOM mutations are expensive compared to JavaScript operations. Every mutation can trigger:

Reflow / Layout. The browser recalculates element positions, sizes, and visibility. Changing dimensions, position, content, or visibility of an element triggers reflow of that element and possibly its ancestors and descendants.

Repaint. After layout, the browser repaints the affected pixels. Changes that don't affect layout (like color or opacity) trigger repaint but skip reflow.

Composite. Combining painted layers into the final image. Some properties (like transform and opacity) only trigger composite, making them ideal for animations.

The performance hierarchy from cheap to expensive:

  1. Composite-only properties (transform, opacity) — fastest
  2. Paint-only properties (color, background-color) — moderate
  3. Layout-triggering properties (width, height, position, content changes) — slowest

A few practical rules from this:

Batch DOM reads and writes. Don't read layout, then write, then read, then write. Each read after a write forces the browser to flush pending writes to get accurate measurements. Group all reads first, then all writes.

Animate with transforms. Animating width triggers layout on every frame. Animating transform: translateX() skips layout. The difference is dramatic for complex animations.

Use requestAnimationFrame for visual updates. Topic 12.6 covers this. Browser-coordinated frame timing is more efficient than setTimeout.

Don't add elements one at a time when adding many. Use DocumentFragment or build the subtree in memory.

Interactive animation: Layout thrashing - interleaved reads and writes against all the reads first, then all the writes.

A Note on Frameworks

In modern web development, most teams don't manipulate the DOM directly. Frameworks like React, Vue, and Svelte build a "virtual DOM" or compile to optimized DOM operations. You describe what the page should look like; the framework figures out the minimal DOM changes to make it so.

Direct DOM manipulation still appears in:

For frontend interviews, knowing the underlying DOM API matters even if you use frameworks daily. Senior interviewers often ask "how does the DOM API work without React?" — to see whether you understand what your framework is doing for you.

Common Pitfalls

Interactive animation: The comment box that runs strangers' code - the same user input runs through innerHTML but only displays through textContent.

Warning: The DOM mutation traps.

Using innerHTML for everything. It's convenient but has XSS risks and is slower than property-based modification for simple changes. Use textContent and property setters for most cases.

Modifying many elements in a loop without batching. Each modification can trigger layout. Batch with DocumentFragment, or build modifications in memory first.

Reading layout properties (like offsetWidth) after modifying styles. This forces a synchronous layout flush. Batch your reads before your writes.

Forgetting that remove() doesn't always free memory. If you still have JavaScript references to a removed element (in event handlers, closures, or arrays), it's not garbage-collected. Removing from the DOM is necessary but not sufficient.

Why This Matters in Interviews

Interview: DOM mutation questions are common in frontend interviews, especially for roles that involve performance or accessibility work. The questions often probe whether you understand reflow/repaint, what triggers layout, and how to batch updates.

Topic 12.7 — Interview Lens: Browser and DOM Questions — covers the deeper interview-prep treatment.

Try It

Exercise: Build a small dynamic list in vanilla JavaScript:

  1. Create an unordered list with three initial items
  2. Add a button that appends a new item to the list
  3. Add a button that removes the last item
  4. Add a button that toggles a "highlighted" class on every item
  5. Add a button that uses DocumentFragment to add 100 items at once

Open browser DevTools' Performance panel and record while each button runs. Compare the timing of adding 100 items individually vs through a DocumentFragment. The difference is the lesson.

Back to JavaScript Basic to Advance