Skip to main content

How to Modify the DOM in JavaScript

Reading the DOM is only half the story. The real power of JavaScript in the browser comes from modifying the document: creating new elements, inserting them into the page, moving them around, removing them, and cloning them. Every dynamic web application, from a simple to-do list to a complex single-page app, relies on these operations.

This guide covers every method available for modifying the DOM. You will learn how to create elements and text nodes, insert them at precise positions using both modern and legacy APIs, clone existing nodes, batch insertions for performance, and avoid the pitfalls of document.write(). The final section covers the critical topic of reflows and repaints, and how to minimize them for smooth, performant DOM updates.

Creating Elements

Before you can add anything to the page, you need to create it. The DOM provides two primary creation methods.

document.createElement(tag)

Creates a new element node with the specified tag name. The element exists in memory but is not yet part of the document. You must explicitly insert it into the DOM for it to appear on the page.

// Create a new <div> element
let div = document.createElement("div");

// The element exists but is not visible: it's detached from the DOM
console.log(div); // <div></div>
console.log(div.parentNode); // null (not attached anywhere)

// Configure it before inserting
div.id = "notification";
div.className = "alert alert-success";
div.textContent = "Operation completed successfully!";

console.log(div.outerHTML);
// '<div id="notification" class="alert alert-success">Operation completed successfully!</div>'

You can set any property or attribute on the element before inserting it:

let img = document.createElement("img");
img.src = "/images/photo.jpg";
img.alt = "A beautiful landscape";
img.width = 400;
img.height = 300;
img.classList.add("rounded", "shadow");

let link = document.createElement("a");
link.href = "https://example.com";
link.target = "_blank";
link.rel = "noopener noreferrer";
link.textContent = "Visit Example";

let input = document.createElement("input");
input.type = "email";
input.name = "user-email";
input.placeholder = "Enter your email";
input.required = true;

document.createTextNode(text)

Creates a text node containing the specified string. Like createElement, it is not attached to the document until you insert it.

let textNode = document.createTextNode("Hello, World!");
console.log(textNode); // "Hello, World!"
console.log(textNode.nodeType); // 3 (Node.TEXT_NODE)

In practice, you rarely need createTextNode directly because methods like append and textContent handle text insertion for you. But it is useful when you need to insert text alongside elements as separate nodes:

let p = document.createElement("p");

// Build: <p>Click <a href="/help">here</a> for help.</p>
p.appendChild(document.createTextNode("Click "));

let link = document.createElement("a");
link.href = "/help";
link.textContent = "here";
p.appendChild(link);

p.appendChild(document.createTextNode(" for help."));

console.log(p.outerHTML);
// '<p>Click <a href="/help">here</a> for help.</p>'

Inserting Nodes: Modern Methods

The modern insertion API provides five methods that cover every possible position where you might want to insert content. These methods are available on all element nodes and accept multiple arguments, each of which can be either a node or a string. Strings are automatically converted to text nodes.

element.append(...nodesOrStrings)

Inserts one or more nodes or strings at the end of the element's children (inside the element, after the last child).

<ul id="list">
<li>First</li>
</ul>
let list = document.getElementById("list");

// Append a new element
let li = document.createElement("li");
li.textContent = "Second";
list.append(li);

// Append a string (becomes a text node)
list.append("Some text");

// Append multiple items at once
let li3 = document.createElement("li");
li3.textContent = "Third";
let li4 = document.createElement("li");
li4.textContent = "Fourth";
list.append(li3, li4);

Result:

<ul id="list">
<li>First</li>
<li>Second</li>
Some text
<li>Third</li>
<li>Fourth</li>
</ul>

element.prepend(...nodesOrStrings)

Inserts one or more nodes or strings at the beginning of the element's children (inside the element, before the first child).

<ul id="list">
<li>Second</li>
<li>Third</li>
</ul>
let list = document.getElementById("list");

let li = document.createElement("li");
li.textContent = "First";
list.prepend(li);

Result:

<ul id="list">
<li>First</li>
<li>Second</li>
<li>Third</li>
</ul>

element.before(...nodesOrStrings)

Inserts one or more nodes or strings before the element itself (as a previous sibling).

<div id="container">
<p id="target">Existing paragraph</p>
</div>
let target = document.getElementById("target");

let heading = document.createElement("h2");
heading.textContent = "Section Title";
target.before(heading);

Result:

<div id="container">
<h2>Section Title</h2>
<p id="target">Existing paragraph</p>
</div>

element.after(...nodesOrStrings)

Inserts one or more nodes or strings after the element itself (as a next sibling).

let target = document.getElementById("target");

let note = document.createElement("small");
note.textContent = "Added after the paragraph";
target.after(note);

Result:

<div id="container">
<h2>Section Title</h2>
<p id="target">Existing paragraph</p>
<small>Added after the paragraph</small>
</div>

element.replaceWith(...nodesOrStrings)

Replaces the element with the provided nodes or strings. The original element is removed from the DOM.

<div id="container">
<p id="old">Old content</p>
</div>
let old = document.getElementById("old");

let newHeading = document.createElement("h2");
newHeading.textContent = "New content";
old.replaceWith(newHeading);

// The variable `old` still references the detached element
console.log(old.parentNode); // null (removed from DOM)

Result:

<div id="container">
<h2>New content</h2>
</div>

Visual Summary of Positions

<!-- before -->
<div id="parent">
<!-- prepend (first inside) -->
<p>Existing child</p>
<!-- append (last inside) -->
</div>
<!-- after -->

Passing Strings vs. Nodes

All five methods accept both nodes and strings. Strings are automatically converted to text nodes (not parsed as HTML):

let div = document.getElementById("container");

// Strings become text nodes: HTML is NOT parsed
div.append("<strong>Bold?</strong>");
// Result: the literal text "<strong>Bold?</strong>" appears on screen
// Not a bold element: just escaped text

// To insert actual elements, create them first
let strong = document.createElement("strong");
strong.textContent = "Bold!";
div.append(strong);
// Result: Bold! (actually bold)
tip

This automatic text escaping is a security feature. It prevents accidental XSS when you insert user-provided data. If you need to insert HTML strings, use insertAdjacentHTML (covered next).

Moving Existing Elements

A node can only exist in one place in the DOM at a time. If you insert a node that is already in the document, it is automatically moved (not copied) to the new position:

<ul id="list">
<li id="a">A</li>
<li id="b">B</li>
<li id="c">C</li>
</ul>
let list = document.getElementById("list");
let itemA = document.getElementById("a");

// Move item A to the end of the list
list.append(itemA);

Result:

<ul id="list">
<li id="b">B</li>
<li id="c">C</li>
<li id="a">A</li>
</ul>

No copy was made. Item A was removed from its original position and inserted at the end. To keep a copy in the original position, you need to clone the node first (covered later).

insertAdjacentHTML, insertAdjacentText, insertAdjacentElement

These three methods provide precise positioning like before, after, prepend, and append, but with one key difference: insertAdjacentHTML accepts an HTML string and parses it into real DOM nodes. This makes it the safe and efficient alternative to innerHTML +=.

insertAdjacentHTML(position, htmlString)

Parses the HTML string and inserts the resulting nodes at the specified position. The existing DOM content is not destroyed.

The position parameter is one of four string values:

PositionDescription
"beforebegin"Before the element itself (previous sibling)
"afterbegin"Inside the element, before the first child
"beforeend"Inside the element, after the last child
"afterend"After the element itself (next sibling)

Visual reference:

<!-- "beforebegin" -->
<div id="target">
<!-- "afterbegin" -->
<p>Existing content</p>
<!-- "beforeend" -->
</div>
<!-- "afterend" -->
let target = document.getElementById("target");

target.insertAdjacentHTML("beforebegin", "<h2>Title Above</h2>");
target.insertAdjacentHTML("afterbegin", "<p>First inside</p>");
target.insertAdjacentHTML("beforeend", "<p>Last inside</p>");
target.insertAdjacentHTML("afterend", "<footer>Below</footer>");

Result:

<h2>Title Above</h2>
<div id="target">
<p>First inside</p>
<p>Existing content</p>
<p>Last inside</p>
</div>
<footer>Below</footer>

Why insertAdjacentHTML Is Better Than innerHTML +=

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

// ❌ innerHTML += destroys existing content and recreates everything
container.innerHTML += "<p>New item</p>";
// All event listeners on existing children are LOST
// All form input values are RESET
// All references to existing children become STALE

// ✅ insertAdjacentHTML adds without destroying
container.insertAdjacentHTML("beforeend", "<p>New item</p>");
// Existing children, their event listeners, and form values are preserved
warning

insertAdjacentHTML still parses HTML, so it carries the same XSS risk as innerHTML when used with untrusted data. Never insert user input directly:

let userInput = '<img src="x" onerror="alert(\'XSS\')">';

// ❌ DANGEROUS
container.insertAdjacentHTML("beforeend", userInput);

// ✅ SAFE: use insertAdjacentText for untrusted strings
container.insertAdjacentText("beforeend", userInput);
// Inserts literal text, no HTML parsing

insertAdjacentText(position, text)

Works exactly like insertAdjacentHTML but treats the string as plain text (no HTML parsing). Safe for user input.

let div = document.getElementById("box");

div.insertAdjacentText("beforeend", "Hello, <World>!");
// Inserts the literal text: Hello, <World>!
// The angle brackets are NOT parsed as HTML

insertAdjacentElement(position, element)

Inserts an existing element node at the specified position. This is similar to using before, after, prepend, or append, but uses the position-string syntax:

let target = document.getElementById("target");

let newDiv = document.createElement("div");
newDiv.textContent = "New element";

target.insertAdjacentElement("afterend", newDiv);
// Same as: target.after(newDiv)

Removing Nodes

node.remove()

The simplest way to remove a node from the DOM. Call remove() directly on the element you want to delete:

let element = document.getElementById("notification");
element.remove();

// The element is removed from the page
// The variable still references the detached element
console.log(element.id); // "notification": still accessible in memory
console.log(element.parentNode); // null: no longer in the DOM

Removing Multiple Elements

// Remove all elements with a specific class
let items = document.querySelectorAll(".removable");
items.forEach(item => item.remove());

// Remove all children of an element
let container = document.getElementById("container");
while (container.firstChild) {
container.firstChild.remove();
}

// Or simply clear everything with innerHTML
container.innerHTML = "";

// Or use replaceChildren() with no arguments
container.replaceChildren();

Temporarily Removing and Re-Inserting

Since remove() detaches the element but does not destroy the JavaScript reference, you can re-insert the element later:

let notification = document.getElementById("notification");

// Remove from the page
notification.remove();

// Later, re-insert it
document.body.append(notification);

Moving an Element (Remove + Insert in One Step)

As mentioned earlier, inserting an element that is already in the DOM automatically removes it from its previous location. So moving is a single operation:

let item = document.getElementById("item-3");
let targetList = document.getElementById("completed-list");

// This removes item from its current parent and inserts it into targetList
targetList.append(item);

Cloning Nodes: cloneNode(deep)

The cloneNode method creates a copy of a node. It takes a single boolean parameter that determines whether to also clone the node's children.

Shallow Clone: cloneNode(false)

Copies only the element itself with its attributes, but not its children:

<div id="card" class="card featured" data-id="42">
<h2>Title</h2>
<p>Content goes here</p>
</div>
let card = document.getElementById("card");
let shallowClone = card.cloneNode(false);

console.log(shallowClone.outerHTML);
// '<div id="card" class="card featured" data-id="42"></div>'
// Note: no children: the <h2> and <p> are not copied

Deep Clone: cloneNode(true)

Copies the element, all its attributes, and all its descendants (the entire subtree):

let card = document.getElementById("card");
let deepClone = card.cloneNode(true);

console.log(deepClone.outerHTML);
// '<div id="card" class="card featured" data-id="42">
// <h2>Title</h2>
// <p>Content goes here</p>
// </div>'

Important: Clones Are Independent

The clone is a completely separate node. Modifying the clone does not affect the original, and vice versa:

let original = document.getElementById("card");
let clone = original.cloneNode(true);

clone.id = "card-copy"; // Change the clone's ID
clone.querySelector("h2").textContent = "Cloned Title";

console.log(original.querySelector("h2").textContent); // "Title" (unchanged)
console.log(clone.querySelector("h2").textContent); // "Cloned Title"

What cloneNode Does NOT Copy

There are important things that cloneNode does not duplicate:

  • Event listeners added with addEventListener are NOT copied
  • JavaScript properties added manually (like element.myData = ...) are NOT copied
  • The value property of <input> elements reflects the current typed value, and cloneNode copies the HTML attribute, not the current property value
let original = document.getElementById("myInput");
original.addEventListener("click", () => console.log("clicked!"));
original.myCustomData = { important: true };

let clone = original.cloneNode(true);

// ❌ Event listener is NOT on the clone
// ❌ clone.myCustomData is undefined
console.log(clone.myCustomData); // undefined
caution

Duplicate IDs: When you clone an element with an id, both the original and the clone have the same id. Since IDs must be unique in a document, always change or remove the ID on the clone before inserting it:

let card = document.getElementById("card");
let clone = card.cloneNode(true);

// Fix the duplicate ID before inserting
clone.id = "card-2";
// or
clone.removeAttribute("id");

document.body.append(clone);

Practical Example: Template-Based Cloning

<template id="card-template">
<div class="card">
<h3 class="card-title"></h3>
<p class="card-body"></p>
<button class="card-action">Read More</button>
</div>
</template>

<div id="card-container"></div>
let template = document.getElementById("card-template");
let container = document.getElementById("card-container");

let articles = [
{ title: "JavaScript Basics", body: "Learn the fundamentals..." },
{ title: "DOM Manipulation", body: "Modify the page dynamically..." },
{ title: "Event Handling", body: "Respond to user actions..." }
];

articles.forEach(article => {
// Clone the template content (deep clone)
let clone = template.content.cloneNode(true);

// Fill in the data
clone.querySelector(".card-title").textContent = article.title;
clone.querySelector(".card-body").textContent = article.body;

// Insert into the page
container.append(clone);
});

DocumentFragment: Batch Insertions

A DocumentFragment is a lightweight, minimal document object that acts as a temporary container for nodes. It is not part of the active DOM tree. When you append a DocumentFragment to the DOM, only its children are inserted (the fragment itself disappears). This makes it useful for assembling a group of nodes before inserting them all at once.

Creating and Using a DocumentFragment

let fragment = document.createDocumentFragment();

// Add multiple elements to the fragment
for (let i = 1; i <= 5; i++) {
let li = document.createElement("li");
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}

// The fragment holds 5 <li> elements
console.log(fragment.childNodes.length); // 5

// Insert all of them into the DOM in one operation
let ul = document.getElementById("list");
ul.appendChild(fragment);

// The fragment is now empty: its children moved to the <ul>
console.log(fragment.childNodes.length); // 0

Why Use DocumentFragment?

The historical motivation was performance. Each time you insert a node into the live DOM, the browser may need to recalculate styles, layout, and repaint. By building everything in a fragment first, you trigger only one layout recalculation when the fragment is finally inserted.

However, modern browsers batch DOM updates efficiently. In most cases, you can achieve the same result with the append method, which accepts multiple arguments:

let ul = document.getElementById("list");
let items = [];

for (let i = 1; i <= 5; i++) {
let li = document.createElement("li");
li.textContent = `Item ${i}`;
items.push(li);
}

// Modern approach: append accepts multiple nodes
ul.append(...items);

When DocumentFragment Still Matters

DocumentFragment remains useful in a few situations:

Working with the <template> element: The content property of <template> is a DocumentFragment. Cloning it is the standard way to instantiate templates.

let template = document.getElementById("row-template");
let clone = template.content.cloneNode(true); // Returns a DocumentFragment
tableBody.appendChild(clone);

APIs that require a single node: Some APIs or library functions expect a single node argument. A DocumentFragment lets you wrap multiple nodes into one argument.

Very large insertions: When inserting hundreds or thousands of nodes, using a fragment still provides a measurable performance benefit on some browsers.

// Building a large table
let fragment = document.createDocumentFragment();

for (let row = 0; row < 1000; row++) {
let tr = document.createElement("tr");
for (let col = 0; col < 10; col++) {
let td = document.createElement("td");
td.textContent = `${row},${col}`;
tr.appendChild(td);
}
fragment.appendChild(tr);
}

// One DOM insertion for 1000 rows
document.querySelector("tbody").appendChild(fragment);

Legacy Methods: appendChild, insertBefore, replaceChild, removeChild

Before the modern append, prepend, before, after, replaceWith, and remove methods existed, DOM manipulation relied on a set of older methods. You will encounter these frequently in older codebases, tutorials, and libraries.

parentNode.appendChild(node)

Appends a child node to the end of the parent's children. Returns the appended node.

let list = document.getElementById("list");
let li = document.createElement("li");
li.textContent = "New item";

let addedNode = list.appendChild(li);
console.log(addedNode === li); // true (returns the inserted node)

Differences from append:

  • appendChild accepts only one node (not multiple, not strings)
  • appendChild returns the inserted node
  • append accepts multiple arguments (nodes and strings) and returns undefined
// appendChild: one node only, returns it
let node = parent.appendChild(child);

// append: multiple nodes and strings, returns undefined
parent.append(child1, child2, "some text");

parentNode.insertBefore(newNode, referenceNode)

Inserts newNode before referenceNode within parentNode. If referenceNode is null, the node is appended at the end (like appendChild).

let list = document.getElementById("list");
let secondItem = list.children[1]; // The second <li>

let newItem = document.createElement("li");
newItem.textContent = "Inserted before second";

list.insertBefore(newItem, secondItem);

There is no legacy insertAfter method. To insert after a reference node, you use insertBefore with the reference node's nextSibling:

// Insert newNode after referenceNode
function insertAfter(newNode, referenceNode) {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}

This awkward pattern is exactly why the modern after method was created.

parentNode.replaceChild(newChild, oldChild)

Replaces oldChild with newChild. Returns the old (replaced) child.

let container = document.getElementById("container");
let oldParagraph = container.querySelector("p");

let newHeading = document.createElement("h2");
newHeading.textContent = "Replaced!";

let replaced = container.replaceChild(newHeading, oldParagraph);
console.log(replaced === oldParagraph); // true
console.log(replaced.parentNode); // null (detached)

parentNode.removeChild(node)

Removes node from parentNode. Returns the removed node.

let list = document.getElementById("list");
let firstItem = list.firstElementChild;

let removed = list.removeChild(firstItem);
console.log(removed.textContent); // The text of the removed item
console.log(removed.parentNode); // null

Legacy vs. Modern: Comparison

TaskLegacyModern
Append childparent.appendChild(node)parent.append(node)
Prepend childparent.insertBefore(node, parent.firstChild)parent.prepend(node)
Insert beforeparent.insertBefore(node, ref)ref.before(node)
Insert afterparent.insertBefore(node, ref.nextSibling)ref.after(node)
Replaceparent.replaceChild(newNode, oldNode)oldNode.replaceWith(newNode)
Removeparent.removeChild(node)node.remove()
Append textparent.appendChild(document.createTextNode(str))parent.append(str)
Append multipleMultiple appendChild callsparent.append(a, b, c)

The modern methods are shorter, more readable, and more versatile. Use them in new code. Understand the legacy methods for reading older codebases.

replaceChildren(...nodesOrStrings)

A relatively recent addition that replaces all children of an element. It combines removing all existing children and appending new ones in a single call:

let list = document.getElementById("list");

// Replace all children with new content
let li1 = document.createElement("li");
li1.textContent = "New First";
let li2 = document.createElement("li");
li2.textContent = "New Second";

list.replaceChildren(li1, li2);
// All previous children are removed, li1 and li2 are the only children now

// Call with no arguments to remove all children
list.replaceChildren(); // Empty the list

document.write(): Why to Avoid It

The document.write() method writes a string of HTML directly into the page. It is one of the oldest JavaScript methods and was commonly used in the early days of the web. Today, it should almost never be used.

How It Works During Page Loading

If called while the page is still loading (during HTML parsing), document.write inserts its content into the document stream at the point where the script executes:

<body>
<h1>Before</h1>
<script>
document.write("<p>Written by JavaScript</p>");
</script>
<h1>After</h1>
</body>

Result:

<body>
<h1>Before</h1>
<p>Written by JavaScript</p>
<h1>After</h1>
</body>

The Destructive Behavior After Loading

If called after the page has finished loading, document.write completely destroys the entire page and replaces it with the new content:

// After the page is fully loaded...
setTimeout(() => {
document.write("<h1>Everything is gone!</h1>");
// The ENTIRE page (head, body, everything) is replaced
// All scripts, styles, and content are destroyed
}, 3000);

This is the primary reason to avoid it. A single misplaced document.write call can wipe out the entire page.

Other Reasons to Avoid document.write

It blocks HTML parsing. When the parser encounters a <script> that calls document.write, it must stop parsing, execute the script, write the content, and then resume parsing. This slows down page loading.

It cannot insert content at specific positions. Unlike appendChild or insertAdjacentHTML, you have no control over where the content goes. It always goes at the current parser position.

It does not work with XHTML or XML documents.

Modern browsers may block it. Chrome blocks document.write calls that insert cross-origin scripts under certain network conditions (slow connections) to improve page load performance.

warning

Never use document.write() in modern code. Use innerHTML, textContent, append, insertAdjacentHTML, or any other DOM method instead. The only remaining (questionable) use case is injecting third-party scripts during page load, and even that has better alternatives (async/defer script tags, dynamic createElement("script")).

// ❌ Never do this
document.write('<script src="analytics.js"></script>');

// ✅ Do this instead
let script = document.createElement("script");
script.src = "analytics.js";
document.head.append(script);

Performance: Minimizing Reflows and Repaints

When you modify the DOM, the browser needs to update what the user sees. This involves two expensive operations:

  • Reflow (Layout): The browser recalculates the positions and dimensions of elements. Any change that affects geometry (adding/removing elements, changing dimensions, modifying text content) triggers a reflow.

  • Repaint: The browser redraws pixels on the screen. Changes to visual properties (colors, shadows, visibility) trigger a repaint without necessarily causing a reflow.

Reflows are more expensive than repaints because they can cascade: changing one element's size might shift all its siblings and descendants.

What Triggers Reflow

// These all trigger reflow:
element.style.width = "200px";
element.style.display = "none";
element.textContent = "New text";
parent.appendChild(newChild);
element.remove();
element.classList.add("wider");

// Reading layout properties ALSO triggers reflow
// (the browser must flush pending changes to give you accurate values)
let width = element.offsetWidth;
let rect = element.getBoundingClientRect();
let height = element.clientHeight;

Batch DOM Reads and Writes

The worst performance pattern is interleaving reads and writes. Each read forces the browser to flush all pending writes, triggering a reflow:

// ❌ BAD: Interleaved reads and writes (triggers reflow on every iteration)
let items = document.querySelectorAll(".item");
items.forEach(item => {
let width = item.offsetWidth; // READ → forces reflow
item.style.width = width * 2 + "px"; // WRITE → schedules another reflow
});
// Each iteration: read → reflow → write → (next iteration) read → reflow → write...
// ✅ GOOD: Batch reads first, then batch writes
let items = document.querySelectorAll(".item");

// Step 1: Read all values (one reflow at most)
let widths = Array.from(items).map(item => item.offsetWidth);

// Step 2: Write all values (one reflow at the end)
items.forEach((item, i) => {
item.style.width = widths[i] * 2 + "px";
});

Build Off-DOM, Then Insert

When creating multiple elements, build the entire structure in memory before inserting it into the DOM. This triggers only one reflow instead of one per insertion:

// ❌ BAD: 100 individual DOM insertions → potentially 100 reflows
let ul = document.getElementById("list");
for (let i = 0; i < 100; i++) {
let li = document.createElement("li");
li.textContent = `Item ${i}`;
ul.appendChild(li); // Triggers layout each time
}
// ✅ GOOD: Build in a fragment, insert once → 1 reflow
let ul = document.getElementById("list");
let fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
let li = document.createElement("li");
li.textContent = `Item ${i}`;
fragment.appendChild(li); // No reflow: fragment is not in the DOM
}

ul.appendChild(fragment); // One reflow
// ✅ ALSO GOOD: Use innerHTML for bulk HTML (one parse + one reflow)
let ul = document.getElementById("list");
let html = "";

for (let i = 0; i < 100; i++) {
html += `<li>Item ${i}</li>`;
}

ul.innerHTML = html; // One parse, one reflow

Hide, Modify, Show

For complex updates to an existing element, you can temporarily hide it, make all your changes, then show it again. This converts multiple reflows into two (one for hide, one for show):

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

// Hide the element (one reflow)
container.style.display = "none";

// Make many changes without triggering reflows
container.querySelector("h1").textContent = "New Title";
container.querySelector("p").textContent = "New content";
container.querySelector("img").src = "new-image.jpg";
container.style.backgroundColor = "#f0f0f0";
// ... many more changes ...

// Show the element (one reflow)
container.style.display = "";

Use CSS Classes Instead of Individual Style Changes

Each element.style.x = y assignment potentially triggers a reflow. Applying a CSS class applies all changes at once:

// ❌ Multiple style changes → multiple potential reflows
element.style.width = "200px";
element.style.height = "100px";
element.style.margin = "10px";
element.style.padding = "20px";
element.style.border = "1px solid #ccc";

// ✅ One class change → one reflow
element.classList.add("card-expanded");
// All styles are defined in CSS:
// .card-expanded { width: 200px; height: 100px; margin: 10px; padding: 20px; border: 1px solid #ccc; }

Use requestAnimationFrame for Visual Updates

When making visual changes in response to events like scrolling or resizing, use requestAnimationFrame to batch your updates with the browser's render cycle:

// ❌ Scroll handler fires many times, each one does DOM work
window.addEventListener("scroll", () => {
header.style.opacity = Math.max(0, 1 - scrollY / 300);
header.style.transform = `translateY(${scrollY * 0.5}px)`;
});

// ✅ Debounce with requestAnimationFrame
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
header.style.opacity = Math.max(0, 1 - scrollY / 300);
header.style.transform = `translateY(${scrollY * 0.5}px)`;
ticking = false;
});
ticking = true;
}
});

Performance Tips Summary

TechniqueBenefit
Batch reads, then writesAvoids forced synchronous reflows
Build in DocumentFragment or array, insert onceOne reflow instead of N
Use innerHTML for bulk HTMLOne parse and reflow
Hide → modify → showLimits reflows to 2
Use CSS classes over inline stylesOne reflow for multiple style changes
Use requestAnimationFrameSyncs with browser render cycle
Avoid reading layout properties in loopsPrevents forced reflow per iteration

Complete Practical Example

Here is a comprehensive example that demonstrates creation, insertion, cloning, removal, and performance-conscious DOM manipulation:

<!DOCTYPE html>
<html>
<head>
<title>DOM Modification Demo</title>
<style>
.task { padding: 8px; margin: 4px 0; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
.task.completed { opacity: 0.5; text-decoration: line-through; }
.btn { cursor: pointer; padding: 4px 8px; border: none; border-radius: 4px; }
.btn-done { background: #4caf50; color: white; }
.btn-delete { background: #f44336; color: white; }
.btn-clone { background: #2196f3; color: white; }
</style>
</head>
<body>
<h1>Task Manager</h1>
<div>
<input type="text" id="task-input" placeholder="New task...">
<button id="add-btn">Add Task</button>
<button id="bulk-add-btn">Add 100 Tasks</button>
<button id="clear-btn">Clear All</button>
</div>
<div id="task-list"></div>
<p id="counter">0 tasks</p>

<script>
let taskList = document.getElementById("task-list");
let input = document.getElementById("task-input");
let counter = document.getElementById("counter");

// Create a single task element
function createTask(text) {
let task = document.createElement("div");
task.className = "task";

let span = document.createElement("span");
// textContent is safe from XSS
span.textContent = text;

let actions = document.createElement("div");

let doneBtn = document.createElement("button");
doneBtn.className = "btn btn-done";
doneBtn.textContent = "✓";

let cloneBtn = document.createElement("button");
cloneBtn.className = "btn btn-clone";
cloneBtn.textContent = "⧉";

let deleteBtn = document.createElement("button");
deleteBtn.className = "btn btn-delete";
deleteBtn.textContent = "✕";

actions.append(doneBtn, " ", cloneBtn, " ", deleteBtn);
task.append(span, actions);

return task;
}

// Add a single task
document.getElementById("add-btn").addEventListener("click", () => {
let text = input.value.trim();
if (!text) return;

let task = createTask(text);
taskList.append(task);

input.value = "";
input.focus();
updateCounter();
});

// Bulk add using DocumentFragment for performance
document.getElementById("bulk-add-btn").addEventListener("click", () => {
let fragment = document.createDocumentFragment();

for (let i = 1; i <= 100; i++) {
let task = createTask(`Bulk task #${i}`);
fragment.appendChild(task);
}

// One DOM insertion for 100 tasks
taskList.appendChild(fragment);
updateCounter();
});

// Clear all using replaceChildren
document.getElementById("clear-btn").addEventListener("click", () => {
taskList.replaceChildren();
updateCounter();
});

// Event delegation for task actions
taskList.addEventListener("click", (event) => {
let btn = event.target.closest(".btn");
if (!btn) return;

let task = btn.closest(".task");

if (btn.classList.contains("btn-done")) {
task.classList.toggle("completed");
}

if (btn.classList.contains("btn-clone")) {
let clone = task.cloneNode(true);
// Insert clone right after the original using after()
task.after(clone);
updateCounter();
}

if (btn.classList.contains("btn-delete")) {
task.remove();
updateCounter();
}
});

function updateCounter() {
let count = taskList.children.length;
counter.textContent = `${count} task${count !== 1 ? "s" : ""}`;
}
</script>
</body>
</html>

This example brings together:

  • createElement and append for building elements
  • textContent for safe text insertion
  • cloneNode(true) for duplicating tasks (note: event listeners work through delegation, so cloned elements automatically respond to clicks)
  • remove() for deleting tasks
  • replaceChildren() for clearing all tasks
  • DocumentFragment for bulk insertion performance
  • after() for inserting clones in the right position
  • Event delegation so cloned elements work without re-attaching listeners

Summary

JavaScript provides a complete set of methods for creating, inserting, moving, replacing, removing, and cloning DOM nodes.

Creation:

  • document.createElement(tag) creates an element node
  • document.createTextNode(text) creates a text node

Modern Insertion (preferred):

  • parent.append(...nodes) inserts at the end (inside)
  • parent.prepend(...nodes) inserts at the beginning (inside)
  • node.before(...nodes) inserts before the node (sibling)
  • node.after(...nodes) inserts after the node (sibling)
  • node.replaceWith(...nodes) replaces the node
  • All accept multiple arguments, both nodes and strings (strings become text nodes)

HTML String Insertion:

  • element.insertAdjacentHTML(position, html) parses and inserts HTML at one of four positions (beforebegin, afterbegin, beforeend, afterend)
  • element.insertAdjacentText(position, text) inserts plain text (safe for user input)
  • element.insertAdjacentElement(position, element) inserts an element

Removal:

  • node.remove() removes the node from the DOM
  • parent.replaceChildren() removes all children (or replaces them)

Cloning:

  • node.cloneNode(false) copies only the node and its attributes (shallow)
  • node.cloneNode(true) copies the node, attributes, and all descendants (deep)
  • Event listeners and custom JS properties are not cloned

Batch Insertion:

  • DocumentFragment acts as a temporary off-DOM container
  • Insert the fragment once to avoid multiple reflows

Legacy Methods (understand for older code):

  • parent.appendChild(node), parent.insertBefore(node, ref), parent.replaceChild(new, old), parent.removeChild(node)

Avoid:

  • document.write() destroys the entire page if called after loading
  • innerHTML += destroys and recreates all existing content

Performance Rules:

  • Batch reads before writes
  • Build off-DOM, then insert once
  • Use CSS classes over multiple inline style changes
  • Use requestAnimationFrame for visual updates in event handlers

Table of Contents