Chapter 8.3☕ 18 min read

DOM Manipulation: innerHTML XSS, clone & DocumentFragment

innerHTML se hack ho jayega agar user input daala. textContent safe hai. cloneNode listeners copy nahi karta. insertAdjacentHTML behtaar hai.

01Creating Nodes — createElement, createTextNode & cloneNode

document.createElement('div') creates an element NOT attached to the DOM — you must append it later. It exists only in memory until you explicitly add it to the page.

document.createTextNode('hello') creates a text node — rarely needed because textContent is simpler and does the same job.

element.textContent = 'hello' is the simplest way to set text — no text node creation needed, no HTML parsing, no XSS risk.

Elements created with createElement are in memory only — invisible until appended to the DOM. You can set attributes, classes, and styles on the element before appending, which means zero reflows until it's attached.

// Create element — NOT in DOM yet
const div = document.createElement('div');
div.className = 'card';
div.id = 'card-1';
div.textContent = 'Hello Hyderabad!'; // Simple text setting
div.style.color = 'blue';

// Not visible yet! Must append to DOM
document.body.appendChild(div); // NOW it appears

// createTextNode — rarely needed
const textNode = document.createTextNode('Raw text');
div.appendChild(textNode); // Adds text AFTER existing content

// textContent vs createTextNode:
// div.textContent = 'Hello';   ✅ Simple, replaces all children
// div.appendChild(createTextNode('Hello')); ❌ Verbose, appends instead of replacing
02Appending — appendChild, insertBefore & append/prepend

textContent: Returns ALL text including hidden elements. FAST — no reflow. Safe from XSS.

innerText: Returns only VISIBLE text. SLOW — triggers reflow to check visibility! Use sparingly.

innerHTML: Parses HTML string into DOM nodes. SLOWEST. XSS RISK with user input!

innerHTML security: element.innerHTML = userInput allows script injection if userInput contains HTML.

ALWAYS use textContent for user text. Use innerHTML only with trusted HTML (your own templates, not user data).

// ⚠️ XSS ATTACK — NEVER do this!
const userInput = '<img src=x onerror=alert("hacked!")>';
const div = document.getElementById('output');
div.innerHTML = userInput; // CODE EXECUTES! XSS vulnerability!

// ✅ SAFE: textContent escapes everything
div.textContent = userInput; // Shows the raw text. No HTML parsing.

// Performance comparison:
// textContent: Reads text from DOM tree directly. No layout. O(n).
// innerText: Must determine visibility for each element → REFLOW! O(n × layout).
// innerHTML: Serializes DOM to HTML string (read) or parses HTML to DOM (write).

// Reading text from hidden elements
const hidden = document.querySelector('.hidden-element');
hidden.textContent; // Returns text even if hidden ✅
hidden.innerText;   // Returns "" if element is display:none ❌

// When innerHTML IS appropriate (trusted HTML only)
const template = '<div class="card"><h2>Title</h2><p>Content</p></div>';
container.innerHTML = template; // OK — this is YOUR template, not user input
innerText is the MOST expensive DOM read property because it forces a full layout recalculation (reflow) to determine which text is visible. Never use it in loops or performance-sensitive code. Always prefer textContent.
03Removing — removeChild, remove, innerHTML="" & replaceWith

append(...nodes): Accepts multiple arguments AND strings. Does NOT return the appended node.

appendChild(node): Single node only. RETURNS the appended node.

prepend(), before(), after(), replaceWith(): Modern methods — more intuitive than insertBefore.

remove(): Removes element from DOM — no parent reference needed.

removeChild(child): Returns removed child — need parent reference.

cloneNode(false): Shallow — copies element only, no children.

cloneNode(true): Deep — copies element + all descendants. EVENT LISTENERS ARE NEVER CLONED!

// append vs appendChild
const list = document.getElementById('list');
const li1 = document.createElement('li');
const li2 = document.createElement('li');

list.append(li1, li2, 'text node'); // Multiple! Strings auto-converted!
list.appendChild(li1); // Single only. Returns li1.

// Modern insertion methods
const newItem = document.createElement('li');
list.prepend(newItem);            // Insert as FIRST child
li1.before(newItem);              // Insert BEFORE li1 (sibling)
li1.after(newItem);               // Insert AFTER li1 (sibling)
li1.replaceWith(newItem);         // Replace li1 with newItem

// Remove
li1.remove();                     // Simple. No return.
list.removeChild(li2);            // Need parent. Returns li2.

// Clone — EVENT LISTENERS NOT COPIED!
const original = document.getElementById('card');
original.addEventListener('click', () => console.log('clicked'));

const shallow = original.cloneNode(false); // Empty card, no children
const deep = original.cloneNode(true);     // Full copy with children

deep.addEventListener('click', () => console.log('clicked'));
// Must re-attach listeners! They don't clone!
04Inserting HTML — insertAdjacentHTML & innerHTML Pitfalls

XSS (Cross-Site Scripting): Injecting malicious scripts into a webpage through user input.

Attack vector: userInput contains <script> or <img onerror=...> or <svg onload=...>.

innerHTML does NOT execute <script> tags (browser security). BUT it DOES execute onerror, onload, onfocus on elements.

The attack flow: User enters malicious input → stored in DB → displayed via innerHTML → script runs.

Prevention: ALWAYS sanitize user input. Use textContent for text. Use DOMPurify library for HTML.

// XSS attack vectors that WORK with innerHTML:

// Vector 1: img onerror
const attack1 = '<img src="x" onerror="alert(document.cookies)">';
div.innerHTML = attack1; // EXECUTES! Image fails, onerror runs.

// Vector 2: svg onload
const attack2 = '<svg onload="alert(1)">';
div.innerHTML = attack2; // EXECUTES on load!

// Vector 3: autofocus + onfocus
const attack3 = '<input autofocus onfocus="alert(1)">';
div.innerHTML = attack3; // EXECUTES when input gets focus!

// ❌ innerHTML does NOT execute <script> tags
const attack4 = '<script>alert(1)</script>';
div.innerHTML = attack4; // Does NOT execute (browser blocks it)
// But this is NOT safe — other vectors still work!

// ✅ Defense 1: textContent (escapes everything)
div.textContent = userInput; // 100% safe. Shows raw text.

// ✅ Defense 2: DOMPurify (if you need HTML)
// const clean = DOMPurify.sanitize(userInput);
// div.innerHTML = clean; // Strips all dangerous HTML
The #1 rule of DOM manipulation: NEVER use innerHTML with user input. If you must render HTML from users, use DOMPurify (github.com/cure53/DOMPurify) to sanitize it first. For plain text, textContent is always safe and faster.
05DocumentFragment — Virtual Batch DOM Updates

insertAdjacentHTML(position, html): Parse and insert HTML at a specific position — faster than innerHTML for partial updates.

Positions: beforebegin (before element), afterbegin (first child), beforeend (last child), afterend (after element).

innerHTML is faster for LARGE HTML strings (browser's C++ HTML parser vs JS loop).

createElement is safer and more flexible for small numbers of elements.

insertAdjacentHTML does NOT destroy existing children (unlike innerHTML which replaces all).

// insertAdjacentHTML — 4 positions
const card = document.getElementById('card');

card.insertAdjacentHTML('beforebegin', '<div>Before card</div>');
card.insertAdjacentHTML('afterbegin', '<div>First child</div>');
card.insertAdjacentHTML('beforeend', '<div>Last child</div>');
card.insertAdjacentHTML('afterend', '<div>After card</div>');

// ✅ insertAdjacentHTML: Adds WITHOUT destroying existing children
card.insertAdjacentHTML('beforeend', '<p>New paragraph</p>');
// Existing children remain! No re-parsing!

// ❌ innerHTML += : Destroys and re-creates ALL children
card.innerHTML += '<p>New paragraph</p>';
// 1. Serializes existing DOM to string
// 2. Appends new HTML to string
// 3. Re-parses ENTIRE string back to DOM
// 4. ALL event listeners on existing children LOST!

DocumentFragment: A lightweight container for batch DOM operations. Append all children to the fragment first, then append the fragment to the DOM once — single reflow instead of many.

// DocumentFragment — batch DOM operations
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = 'Item ' + i;
  fragment.appendChild(li); // No reflow — not in DOM yet!
}

list.appendChild(fragment); // ONE reflow for all 100 items!
// Fragment disappears — its children move directly into list

// Performance summary:
// Large HTML (100+ elements): innerHTML = faster (C++ parser)
// Small updates (1-5 elements): createElement = safer, same speed
// Partial updates: insertAdjacentHTML = best (no destruction)
// Batch operations: DocumentFragment = best (single reflow)
// User text: textContent = always (XSS-safe)
innerHTML += is one of the most expensive DOM operations. It serializes the entire existing DOM to a string, concatenates, then re-parses. This destroys all event listeners and JS references to existing elements. Use insertAdjacentHTML, append, or DocumentFragment instead.

Lo kar liya — Key Points:

  • ✅ document.createElement creates elements in memory — they're invisible until appended to the DOM
  • ✅ textContent is safe from XSS and fastest for text; innerText triggers reflow; innerHTML parses HTML (XSS risk)
  • ✅ NEVER use innerHTML with user input — use textContent for text or DOMPurify for HTML
  • ✅ append accepts multiple arguments and strings; appendChild accepts single node and returns it
  • ✅ cloneNode(true) copies element and children but NEVER copies event listeners
  • ✅ insertAdjacentHTML inserts HTML at specific positions WITHOUT destroying existing children
  • ✅ innerHTML += destroys all existing event listeners — use insertAdjacentHTML or append instead
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase