Skip to main content
How does JavaScript change what you see on a webpage? How do you click a button and see new content appear, or type in a form and watch suggestions pop up? How does a “dark mode” toggle instantly transform an entire page?
// The DOM lets you do things like this:
document.querySelector('h1').textContent = 'Hello, DOM!'
document.body.style.backgroundColor = 'lightblue'
document.getElementById('btn').addEventListener('click', handleClick)
The Document Object Model (DOM) is the bridge between your HTML and JavaScript. It lets you read, modify, and respond to changes in web page content. With the DOM, you can use methods like querySelector() to find elements, getElementById() to grab specific nodes, and addEventListener() to respond to user interactions.
What you’ll learn in this guide:
  • What the DOM is in JavaScript and how it differs from HTML
  • How to select DOM elements (getElementById vs querySelector)
  • How to traverse the DOM tree (parent, children, siblings)
  • How to manipulate DOM elements (create, modify, remove)
  • The difference between properties and attributes
  • How the browser turns DOM → pixels (the Critical Rendering Path)
  • Performance best practices (avoid layout thrashing!)
Prerequisite: This guide assumes basic familiarity with HTML and CSS. If you’re new to web development, start there first!

What is the DOM in JavaScript?

The Document Object Model (DOM) is a programming interface that represents HTML documents as a tree of objects. As specified by the WHATWG DOM Living Standard, when a browser loads a webpage, it parses the HTML and creates the DOM, a live, structured representation that JavaScript can read and modify. Every element, attribute, and piece of text becomes a node in this tree. In short: the DOM is how JavaScript “sees” and changes a webpage.

How the DOM Tree Structure Works

Think of the DOM like a family tree. At the top sits document (the family historian who knows everyone). Below it is <html> (the matriarch), which has two children: <head> and <body>. Each of these has their own children, grandchildren, and so on.
                           THE DOM FAMILY TREE

                              ┌──────────┐
                              │ document │  ← The family historian
                              │ (root)   │    (knows everyone!)
                              └────┬─────┘

                              ┌────┴─────┐
                              │  <html>  │  ← Great-grandma
                              └────┬─────┘    (the matriarch)
                     ┌─────────────┴─────────────┐
                     │                           │
                ┌────┴────┐                 ┌────┴────┐
                │ <head>  │                 │ <body>  │  ← The two branches
                └────┬────┘                 └────┬────┘    of the family
                     │                           │
              ┌──────┴──────┐         ┌──────────┼──────────┐
              │             │         │          │          │
         ┌────┴────┐  ┌────┴────┐ ┌───┴───┐ ┌────┴────┐ ┌───┴───┐
         │ <title> │  │ <meta>  │ │ <nav> │ │ <main>  │ │<footer>│
         └────┬────┘  └─────────┘ └───┬───┘ └────┬────┘ └───────┘
              │                       │          │
         "My Page"               ┌────┴────┐  ┌──┴──┐
          (text)                 │  <ul>   │  │<div>│  ← Cousins
                                 └────┬────┘  └──┬──┘
                                      │          │
                                 ┌────┼────┐    ...
                                 │    │    │
                               <li> <li> <li>   ← Siblings
Just like navigating a family reunion, the DOM lets you:
ActionFamily AnalogyDOM Method
Find your parent”Who’s your mom?”element.parentNode
Find your kids”Where are your children?”element.children
Find your sibling”Who’s your brother?”element.nextElementSibling
Search the whole family”Where’s cousin Bob?”document.querySelector('#bob')
Key insight: Every element, text, and comment in your HTML becomes a “node” in this tree. JavaScript lets you navigate this tree and modify it: changing content, adding elements, or removing them entirely.

What the DOM is NOT

The DOM is NOT Your HTML Source Code

Here’s the key thing: your HTML file and the DOM are different things:
<!-- What you wrote (invalid HTML - missing head/body) -->
<!DOCTYPE html>
<html>
Hello, World!
</html>
The browser fixes your mistakes! It adds missing <head> and <body> tags, closes unclosed tags, and corrects nesting errors. The DOM is the corrected version. According to the HTML specification’s parsing algorithm, browsers must follow specific error-recovery rules to handle malformed markup consistently across implementations.

The DOM is NOT What You See in DevTools (Exactly)

DevTools shows you something close to the DOM, but it also shows CSS pseudo-elements (::before, ::after) which are NOT part of the DOM:
/* This creates visual content, but NOT DOM nodes */
.quote::before {
  content: '"';
}
Pseudo-elements exist in the render tree (for display), but not in the DOM (for JavaScript). You can’t select them with querySelector!

The DOM is NOT the Render Tree

The Render Tree is what actually gets painted to the screen. It excludes:
<!-- These are in the DOM but NOT in the Render Tree -->
<head>...</head>                    <!-- Never rendered -->
<script>...</script>                <!-- Never rendered -->
<div style="display: none">Hidden</div>  <!-- Excluded from render -->
DOM                          Render Tree
┌─────────────────────┐     ┌─────────────────────┐
│ <html>              │     │ <html>              │
│   <head>            │     │   <body>            │
│     <title>         │     │     <h1>            │
│   <body>            │     │       "Hello"       │
│     <h1>Hello</h1>  │     │     <p>             │
│     <p>World</p>    │     │       "World"       │
│     <div hidden>    │     │                     │
│       Secret!       │     │  (no hidden div!)   │
│     </div>          │     │                     │
└─────────────────────┘     └─────────────────────┘

The document Object: Your Entry Point

The document object is your gateway to the DOM. It’s automatically available in any browser JavaScript. Key properties include document.documentElement (the root <html> element), document.head, document.body, and document.title:
// document is the root of everything
console.log(document)                    // The entire document
console.log(document.documentElement)    // <html> element
console.log(document.head)               // <head> element
console.log(document.body)               // <body> element
console.log(document.title)              // Page title (getter/setter!)

// You can modify the document
document.title = 'New Title'             // Changes browser tab title

DOM Node Types Explained

Everything in the DOM is a Node. But not all nodes are created equal!

The Node Type Hierarchy

                            Node (base class)

        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
    Document             Element              CharacterData
        │                     │                     │
   HTMLDocument          ┌────┴────┐         ┌─────┴─────┐
                         │         │         │           │
                   HTMLElement  SVGElement  Text      Comment

        ┌────────────────┼────────────────┐
        │                │                │
  HTMLDivElement  HTMLSpanElement  HTMLInputElement
                                        ...

Node Types You’ll Encounter

Node TypenodeTypenodeNameExample
Element1Tag name (uppercase)<div>, <p>, <span>
Text3#textText inside elements
Comment8#comment<!-- comment -->
Document9#documentThe document object
DocumentFragment11#document-fragmentVirtual container
const div = document.createElement('div')
console.log(div.nodeType)   // 1 (Element)
console.log(div.nodeName)   // "DIV"

const text = document.createTextNode('Hello')
console.log(text.nodeType)  // 3 (Text)
console.log(text.nodeName)  // "#text"

console.log(document.nodeType)  // 9 (Document)
console.log(document.nodeName)  // "#document"
The createElement() and createTextNode() methods create new nodes that you can add to the DOM.

Node Type Constants

Instead of remembering numbers, use the constants:
Node.ELEMENT_NODE        // 1
Node.TEXT_NODE           // 3
Node.COMMENT_NODE        // 8
Node.DOCUMENT_NODE       // 9
Node.DOCUMENT_FRAGMENT_NODE  // 11

// Check if something is an element
if (node.nodeType === Node.ELEMENT_NODE) {
  console.log('This is an element!')
}

Visualizing a Real DOM Tree

Given this HTML:
<div id="container">
  <h1>Title</h1>
  <!-- A comment -->
  <p>Paragraph</p>
</div>
The actual DOM tree looks like this (including text nodes from whitespace!):
div#container
├── #text (newline + spaces)
├── h1
│   └── #text "Title"
├── #text (newline + spaces)
├── #comment " A comment "
├── #text (newline + spaces)
├── p
│   └── #text "Paragraph"
└── #text (newline)
The Whitespace Gotcha! Line breaks and spaces between HTML tags create text nodes. This surprises many developers! We’ll see how to handle this in the traversal section.

How to Select DOM Elements

Before you can manipulate an element, you need to find it. JavaScript provides several methods through the document object:

The getElementById() Classic

The getElementById() method is the fastest way to select a single element by its unique ID:
// HTML: <div id="hero">Welcome!</div>

const hero = document.getElementById('hero')
console.log(hero)        // <div id="hero">Welcome!</div>
console.log(hero.id)     // "hero"
console.log(hero.textContent)  // "Welcome!"

// Returns null if not found (not an error!)
const ghost = document.getElementById('nonexistent')
console.log(ghost)  // null
IDs must be unique in a document. If you have duplicate IDs, getElementById returns the first one. But don’t do this. It’s invalid HTML!

getElementsByClassName() and getElementsByTagName()

getElementsByClassName() and getElementsByTagName() select multiple elements by class or tag name:
// HTML: 
// <p class="intro">First</p>
// <p class="intro">Second</p>
// <p>Third</p>

const intros = document.getElementsByClassName('intro')
console.log(intros.length)      // 2
console.log(intros[0])          // <p class="intro">First</p>
console.log(intros[0].textContent)  // "First"

const allParagraphs = document.getElementsByTagName('p')
console.log(allParagraphs.length)  // 3

The Modern Way: querySelector() and querySelectorAll()

querySelector() and querySelectorAll() use CSS selectors to find elements. Much more powerful!
// querySelector returns the FIRST match (or null)
const firstButton = document.querySelector('button')      // First <button> element
const submitBtn = document.querySelector('#submit')       // Element with id="submit"
const firstCard = document.querySelector('.card')         // First element with class="card"
const navLink = document.querySelector('nav a.active')    // <a class="active"> inside <nav>
const dataItem = document.querySelector('[data-id="123"]')  // Element with data-id="123"

// querySelectorAll returns ALL matches (NodeList)
const allButtons = document.querySelectorAll('button')    // All <button> elements
const allCards = document.querySelectorAll('.card')       // All elements with class="card"
const evenRows = document.querySelectorAll('tr:nth-child(even)')  // Every even table row

Selector Examples

// By ID
document.querySelector('#main')

// By class
document.querySelector('.active')
document.querySelectorAll('.btn.primary')

// By tag
document.querySelector('header')
document.querySelectorAll('li')

// By attribute
document.querySelector('[type="submit"]')
document.querySelector('[data-modal="login"]')

// Descendant selectors
document.querySelector('nav ul li a')
document.querySelector('.sidebar .widget:first-child')

// Pseudo-selectors (limited support)
document.querySelectorAll('input:not([type="hidden"])')
document.querySelector('p:first-of-type')

Live vs Static Collections

This difference trips up many developers. getElementsByClassName() returns a live HTMLCollection, while querySelectorAll() returns a static NodeList:
const liveList = document.getElementsByClassName('item')    // LIVE HTMLCollection
const staticList = document.querySelectorAll('.item')       // STATIC NodeList

// Start with 3 items
console.log(liveList.length)    // 3
console.log(staticList.length)  // 3

// Add a new item to the DOM
const newItem = document.createElement('div')
newItem.className = 'item'
document.body.appendChild(newItem)

// Check lengths again
console.log(liveList.length)    // 4 (automatically updated!)
console.log(staticList.length)  // 3 (still the old snapshot)
MethodReturnsLive?
getElementById()Element or nullN/A
getElementsByClassName()HTMLCollectionYes (live)
getElementsByTagName()HTMLCollectionYes (live)
querySelector()Element or nullN/A
querySelectorAll()NodeListNo (static)

Scoped Selection

You can call selection methods on any element, not just document:
const nav = document.querySelector('nav')

// Find links ONLY inside nav
const navLinks = nav.querySelectorAll('a')

// Find the active link inside nav
const activeLink = nav.querySelector('.active')
This is faster than searching the entire document and helps avoid selecting unintended elements.

Performance Comparison

In order of speed (fastest first):
  1. getElementById() - Direct hashtable lookup, O(1)
  2. getElementsByClassName() - Optimized internal lookup
  3. getElementsByTagName() - Optimized internal lookup
  4. querySelector() - Must parse CSS selector
  5. querySelectorAll() - Must parse and find all matches
However, for most applications, the difference is negligible. Use querySelector/querySelectorAll for readability unless you’re selecting thousands of elements in a loop.
// Premature optimization - don't do this
const el1 = document.getElementById('myId')

// This is fine and more readable
const el2 = document.querySelector('#myId')

How to Traverse the DOM

Once you have an element, you can navigate to related elements without querying the entire document.

Traversing Downwards (To Children)

const ul = document.querySelector('ul')

// Get ALL child nodes (including text nodes!)
const allChildNodes = ul.childNodes      // NodeList

// Get only ELEMENT children (usually what you want)
const elementChildren = ul.children       // HTMLCollection

// Get specific children
const firstChild = ul.firstChild          // First node (might be text!)
const firstElement = ul.firstElementChild // First ELEMENT child
const lastChild = ul.lastChild            // Last node
const lastElement = ul.lastElementChild   // Last ELEMENT child
The Text Node Trap! Look at this HTML:
<ul>
  <li>One</li>
  <li>Two</li>
</ul>
What is ul.firstChild? It’s NOT the first <li>! It’s a text node containing the newline and spaces after <ul>. Use firstElementChild to get the actual <li> element.

Traversing Upwards (To Parents)

const li = document.querySelector('li')

// Direct parent
const parent = li.parentNode        // Usually same as parentElement
const parentEl = li.parentElement   // Guaranteed to be an Element (or null)

// Find ancestor matching selector (very useful!)
const form = li.closest('form')     // Finds nearest ancestor <form>
const card = li.closest('.card')    // Finds nearest ancestor with class "card"

// closest() includes the element itself
const self = li.closest('li')       // Returns li itself if it matches!
The closest() method is useful for event delegation (see Event Loop for how events are processed):
// Handle clicks on any button inside a card
document.addEventListener('click', (e) => {
  const card = e.target.closest('.card')
  if (card) {
    console.log('Clicked inside card:', card)
  }
})

Traversing Sideways (To Siblings)

const secondLi = document.querySelectorAll('li')[1]

// Previous/next nodes (might be text!)
const prevNode = secondLi.previousSibling
const nextNode = secondLi.nextSibling

// Previous/next ELEMENTS (usually what you want)
const prevElement = secondLi.previousElementSibling
const nextElement = secondLi.nextElementSibling

// Returns null at the boundaries
const firstLi = document.querySelector('li')
console.log(firstLi.previousElementSibling)  // null (no previous sibling)

Node vs Element Properties Cheat Sheet

Get…Node Property (includes text)Element Property (elements only)
ParentparentNodeparentElement
ChildrenchildNodeschildren
First childfirstChildfirstElementChild
Last childlastChildlastElementChild
Previous siblingpreviousSiblingpreviousElementSibling
Next siblingnextSiblingnextElementSibling
Rule of thumb: Unless you specifically need text nodes, always use the Element variants (children, firstElementChild, nextElementSibling, etc.)

Practical Example: Building a Breadcrumb Trail

// Get all ancestors of an element
function getAncestors(element) {
  const ancestors = []
  let current = element.parentElement
  
  while (current && current !== document.body) {
    ancestors.push(current)
    current = current.parentElement
  }
  
  return ancestors
}

const deepElement = document.querySelector('.deeply-nested')
console.log(getAncestors(deepElement))
// [<div.parent>, <section>, <main>, ...]

Creating and Manipulating Elements

The real power of the DOM is the ability to create, modify, and remove elements dynamically.

Creating Elements

Use createElement() to create new elements and createTextNode() to create text nodes:
// Create a new element
const div = document.createElement('div')
const span = document.createElement('span')
const img = document.createElement('img')

// Create a text node
const text = document.createTextNode('Hello, world!')

// Create a comment node
const comment = document.createComment('This is a comment')

// Elements are created "detached" - not yet in the DOM!
console.log(div.parentNode)  // null

Adding Elements to the DOM

There are many ways to add elements. Here’s a comprehensive overview using methods like appendChild(), insertBefore(), append(), and prepend():
Adds a node as the last child of a parent:
const ul = document.querySelector('ul')
const li = document.createElement('li')
li.textContent = 'New item'

ul.appendChild(li)
// <ul>
//   <li>Existing</li>
//   <li>New item</li>  ← Added at the end
// </ul>

insertAdjacentHTML() - The Swiss Army Knife

For inserting HTML strings, insertAdjacentHTML() is powerful and fast:
const div = document.querySelector('div')

// Four positions to insert:
div.insertAdjacentHTML('beforebegin', '<p>Before div</p>')
div.insertAdjacentHTML('afterbegin', '<p>First child of div</p>')
div.insertAdjacentHTML('beforeend', '<p>Last child of div</p>')
div.insertAdjacentHTML('afterend', '<p>After div</p>')
Visual representation:
<!-- beforebegin -->
<div>
  <!-- afterbegin -->
  existing content
  <!-- beforeend -->
</div>
<!-- afterend -->

Removing Elements

Modern and simple. Element removes itself:
const element = document.querySelector('.to-remove')
element.remove()  // Gone!

Cloning Elements

Use cloneNode() to duplicate elements:
const original = document.querySelector('.card')

// Shallow clone (element only, no children)
const shallow = original.cloneNode(false)

// Deep clone (element AND all descendants)
const deep = original.cloneNode(true)

// Clones are detached - must add to DOM
document.body.appendChild(deep)
ID Collision! If you clone an element with an ID, you’ll have duplicate IDs in your document (invalid HTML). Remove or change the ID after cloning:
const clone = original.cloneNode(true)
clone.id = ''  // Remove ID
// or
clone.id = 'new-unique-id'

DocumentFragment - Batch Operations

When adding many elements, using a DocumentFragment is more efficient:
// Bad: Multiple DOM updates (potentially multiple reflows)
const ul = document.querySelector('ul')
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  ul.appendChild(li)  // Modifies live DOM each iteration
}

// Good: Single DOM update
const ul = document.querySelector('ul')
const fragment = document.createDocumentFragment()

for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  fragment.appendChild(li)  // No DOM update (fragment is detached)
}

ul.appendChild(fragment)  // Single DOM update!
A DocumentFragment is a lightweight container that:
  • Is not part of the DOM tree
  • Has no parent
  • When appended, only its children are inserted (the fragment itself disappears)
Modern browser optimization: Browsers may batch consecutive DOM modifications and perform a single reflow. However, using DocumentFragment is still the recommended pattern because it’s explicit, works consistently across all browsers, and avoids any risk of forced synchronous layouts if you read layout properties between writes.

Modifying Content

Three properties let you read and write element content: innerHTML, textContent, and innerText.

innerHTML - Parse and Insert HTML

const div = document.querySelector('div')

// Read HTML content
console.log(div.innerHTML)  // "<p>Hello</p><span>World</span>"

// Write HTML content (parses the string!)
div.innerHTML = '<h1>New Title</h1><p>New paragraph</p>'

// Clear all content
div.innerHTML = ''
Security Alert: XSS Vulnerability!Never use innerHTML with user-provided content:
// DANGEROUS! User could inject: <img src=x onerror="stealCookies()">
div.innerHTML = userInput  // NO!

// Safe alternatives:
div.textContent = userInput  // Escapes HTML
// or sanitize the input first

textContent - Plain Text Only

const div = document.querySelector('div')

// Read text (ignores HTML tags)
// <div><p>Hello</p><span>World</span></div>
console.log(div.textContent)  // "HelloWorld"

// Write text (HTML is escaped, not parsed)
div.textContent = '<script>alert("XSS")</script>'
// Displays literally: <script>alert("XSS")</script>
// Safe from XSS!

innerText - Rendered Text

const div = document.querySelector('div')

// innerText respects CSS visibility
// <div>Hello <span style="display:none">Hidden</span> World</div>

console.log(div.textContent)  // "Hello Hidden World"
console.log(div.innerText)    // "Hello  World" (Hidden is excluded!)

When to Use Each

PropertyUse Case
innerHTMLInserting trusted HTML (never user input!)
textContentSetting/getting plain text (safe, fast)
innerTextGetting text as user sees it (slower, respects CSS)
// Performance: textContent is faster than innerText
// because innerText must calculate styles

// Setting text content (both work, textContent is faster)
element.textContent = 'Hello'  // Preferred
element.innerText = 'Hello'    // Works but slower

How to Work with DOM Attributes

HTML elements have attributes. JavaScript lets you read, write, and remove them using getAttribute(), setAttribute(), hasAttribute(), and removeAttribute().

Standard Attribute Methods

const link = document.querySelector('a')

// Get attribute value
const href = link.getAttribute('href')
const target = link.getAttribute('target')

// Set attribute value
link.setAttribute('href', 'https://example.com')
link.setAttribute('target', '_blank')

// Check if attribute exists
if (link.hasAttribute('target')) {
  console.log('Link opens in new tab')
}

// Remove attribute
link.removeAttribute('target')

Properties vs Attributes: The Difference

This confuses many developers! Attributes are in the HTML. Properties are on the DOM object.
<input type="text" value="initial">
const input = document.querySelector('input')

// ATTRIBUTE: The original HTML value
console.log(input.getAttribute('value'))  // "initial"

// PROPERTY: The current state
console.log(input.value)  // "initial"

// User types "new text"...
console.log(input.getAttribute('value'))  // Still "initial"!
console.log(input.value)                  // "new text"

// Reset to attribute value
input.value = input.getAttribute('value')
Key differences:
AspectAttributeProperty
SourceHTML markupDOM object
Accessget/setAttribute()Direct property access
UpdatesManual onlyAutomatically with user interaction
TypeAlways stringCan be any type
// Attribute is always a string
checkbox.getAttribute('checked')  // "" or null

// Property is a boolean
checkbox.checked  // true or false

// Attribute (string)
input.getAttribute('maxlength')  // "10"

// Property (number)
input.maxLength  // 10

Data Attributes and the dataset API

Custom data attributes start with data- and are accessible via the dataset property:
<div id="user" 
     data-user-id="123" 
     data-role="admin"
     data-is-active="true">
  John Doe
</div>
const user = document.querySelector('#user')

// Read data attributes (camelCase!)
console.log(user.dataset.userId)    // "123"
console.log(user.dataset.role)      // "admin"
console.log(user.dataset.isActive)  // "true" (string, not boolean!)

// Write data attributes
user.dataset.lastLogin = '2024-01-15'
// Creates: data-last-login="2024-01-15"

// Delete data attributes
delete user.dataset.role

// Check if exists
if ('userId' in user.dataset) {
  console.log('Has user ID')
}
Naming Convention: HTML uses kebab-case (data-user-id), JavaScript uses camelCase (dataset.userId). The conversion is automatic!

Common Attribute Shortcuts

Many attributes have direct property shortcuts:
// These pairs are equivalent:
element.id                    // element.getAttribute('id')
element.className             // element.getAttribute('class')
element.href                  // element.getAttribute('href')
element.src                   // element.getAttribute('src')
element.title                 // element.getAttribute('title')

// For class manipulation, use classList (covered next)

How to Style DOM Elements with JavaScript

JavaScript can modify element styles in several ways using the style property and classList API.

The style Property (Inline Styles)

const box = document.querySelector('.box')

// Set individual styles (camelCase!)
box.style.backgroundColor = 'blue'
box.style.fontSize = '20px'
box.style.marginTop = '10px'

// Read styles (only reads INLINE styles!)
console.log(box.style.backgroundColor)  // "blue"
console.log(box.style.color)            // "" (not inline, from stylesheet)

// Set multiple styles at once
box.style.cssText = 'background: red; font-size: 16px; padding: 10px;'

// Remove an inline style
box.style.backgroundColor = ''  // Removes the style
element.style only reads/writes inline styles! To get computed styles (from stylesheets), use getComputedStyle().

getComputedStyle() - Read Actual Styles

Use getComputedStyle() to read the final computed styles:
const box = document.querySelector('.box')

// Get all computed styles
const styles = getComputedStyle(box)

console.log(styles.backgroundColor)  // "rgb(0, 0, 255)"
console.log(styles.fontSize)         // "16px"
console.log(styles.display)          // "block"

// Get pseudo-element styles
const beforeStyles = getComputedStyle(box, '::before')
console.log(beforeStyles.content)    // '"Hello"'

classList - Manipulate CSS Classes

The classList API is the modern way to add/remove/toggle classes:
const button = document.querySelector('button')

// Add classes
button.classList.add('active')
button.classList.add('btn', 'btn-primary')  // Multiple at once

// Remove classes
button.classList.remove('active')
button.classList.remove('btn', 'btn-primary')  // Multiple at once

// Toggle (add if missing, remove if present)
button.classList.toggle('active')

// Toggle with condition
button.classList.toggle('active', isActive)  // Add if isActive is true

// Check if class exists
if (button.classList.contains('active')) {
  console.log('Button is active')
}

// Replace a class
button.classList.replace('btn-primary', 'btn-secondary')

// Iterate over classes
button.classList.forEach(cls => console.log(cls))

// Get number of classes
console.log(button.classList.length)  // 2

className vs classList

// className is a string (old way)
element.className = 'btn btn-primary'     // Replaces ALL classes
element.className += ' active'            // Appending is clunky

// classList is a DOMTokenList (modern way)
element.classList.add('active')           // Adds without affecting others
element.classList.remove('btn-primary')   // Removes specifically

How Browsers Render the DOM to Pixels

Understanding how browsers render pages helps you write performant code. This is where JavaScript Engines and the browser’s rendering engine work together.

From HTML to Pixels

When you load a webpage, the browser goes through these steps:
1

1. Parse HTML → Build DOM

Browser reads HTML bytes and constructs the Document Object Model tree.
2

2. Parse CSS → Build CSSOM

CSS is parsed into the CSS Object Model with styling rules.
3

3. Combine → Render Tree

DOM + CSSOM merge into the Render Tree (only visible elements).
4

4. Layout (Reflow)

Calculate exact position and size of every element.
5

5. Paint

Fill in pixels: colors, borders, shadows, text.
6

6. Composite

Combine layers into the final image using the GPU.
┌─────────────────────────────────────────────────────────────────────────────┐
│                     THE CRITICAL RENDERING PATH                              │
│                                                                              │
│  1. PARSE HTML          2. PARSE CSS           3. BUILD RENDER TREE         │
│  ┌──────────────┐      ┌──────────────┐       ┌──────────────────────┐      │
│  │  HTML bytes  │      │  CSS bytes   │       │  DOM    +   CSSOM    │      │
│  │      ↓       │      │      ↓       │       │    ↘     ↙           │      │
│  │  Characters  │      │  Characters  │       │   RENDER TREE        │      │
│  │      ↓       │      │      ↓       │       │  (visible elements   │      │
│  │   Tokens     │      │   Tokens     │       │   + their styles)    │      │
│  │      ↓       │      │      ↓       │       └──────────────────────┘      │
│  │    Nodes     │      │    Rules     │                  │                  │
│  │      ↓       │      │      ↓       │                  ▼                  │
│  │    DOM       │      │   CSSOM      │       4. LAYOUT (Reflow)            │
│  └──────────────┘      └──────────────┘       ┌──────────────────────┐      │
│                                               │ Calculate exact      │      │
│                                               │ position & size of   │      │
│                                               │ every element        │      │
│                                               └──────────┬───────────┘      │
│                                                          │                  │
│                                                          ▼                  │
│                                               5. PAINT                      │
│                                               ┌──────────────────────┐      │
│                                               │ Fill in pixels:      │      │
│                                               │ colors, borders,     │      │
│                                               │ shadows, text        │      │
│                                               └──────────┬───────────┘      │
│                                                          │                  │
│                                                          ▼                  │
│                                               6. COMPOSITE                  │
│                                               ┌──────────────────────┐      │
│                                               │ Combine layers into  │      │
│                                               │ final image (GPU)    │      │
│                                               └──────────────────────┘      │
│                                                          │                  │
│                                                          ▼                  │
│                                               ┌──────────────────────┐      │
│                                               │      PIXELS!         │      │
│                                               └──────────────────────┘      │
└─────────────────────────────────────────────────────────────────────────────┘

What’s NOT in the Render Tree

The Render Tree only contains visible elements:
<!-- NOT in Render Tree -->
<head>...</head>                    <!-- head is never rendered -->
<script>...</script>                <!-- script tags aren't visible -->
<link rel="stylesheet">             <!-- link tags aren't visible -->
<meta>                              <!-- meta tags aren't visible -->
<div style="display: none">Hi</div> <!-- display:none excluded -->

<!-- IN the Render Tree (even if not seen) -->
<div style="visibility: hidden">Hi</div>  <!-- Takes up space -->
<div style="opacity: 0">Hi</div>          <!-- Takes up space -->

Layout (Reflow) - The Expensive Step

Layout calculates the geometry of every element: position, size, margins, etc. Reflow is triggered when:
  • Adding/removing elements
  • Changing element dimensions (width, height, padding, margin)
  • Changing font size
  • Resizing the window
  • Reading certain properties (more on this below!)

Paint - Drawing Pixels

After layout, the browser paints the pixels: text, colors, images, borders, shadows. Repaint (without reflow) happens when:
  • Changing colors
  • Changing background-image
  • Changing visibility
  • Changing box-shadow (sometimes)

Composite - Layering

Modern browsers separate content into layers and use the GPU to composite them. This is why some animations are smooth:
/* These properties can animate without reflow/repaint */
transform: translateX(100px);  /* GPU accelerated! */
opacity: 0.5;                   /* GPU accelerated! */

/* These properties cause reflow */
left: 100px;    /* Avoid for animations! */
width: 200px;   /* Avoid for animations! */

How to Optimize DOM Performance

DOM operations can be slow. Here’s how to keep your pages fast.

Cache DOM References

// Bad: Queries the DOM every iteration
for (let i = 0; i < 1000; i++) {
  document.querySelector('.result').textContent += i
}

// Good: Query once, reuse
const result = document.querySelector('.result')
for (let i = 0; i < 1000; i++) {
  result.textContent += i
}

// Even better: Build string, set once
const result = document.querySelector('.result')
let text = ''
for (let i = 0; i < 1000; i++) {
  text += i
}
result.textContent = text

Batch DOM Updates

// Avoid: Multiple style changes (may trigger multiple reflows)
element.style.width = '100px'
element.style.height = '200px'
element.style.margin = '10px'

// Better: Single style assignment with cssText
element.style.cssText = 'width: 100px; height: 200px; margin: 10px;'

// Best: Use a CSS class (cleanest and most maintainable)
element.classList.add('my-styles')

// Good: DocumentFragment for multiple elements
const fragment = document.createDocumentFragment()
items.forEach(item => {
  const li = document.createElement('li')
  li.textContent = item
  fragment.appendChild(li)
})
ul.appendChild(fragment)  // Single DOM update
Why batch? While modern browsers often optimize consecutive style changes into a single reflow, this optimization breaks if you read a layout property (like offsetWidth) between writes. Batching explicitly avoids this risk and makes your intent clear.

Avoid Layout Thrashing

Layout thrashing occurs when you alternate between reading and writing DOM properties:
// TERRIBLE: Forces layout on EVERY iteration
boxes.forEach(box => {
  const width = box.offsetWidth      // Read (forces layout)
  box.style.width = (width + 10) + 'px'  // Write (invalidates layout)
})

// GOOD: Batch reads, then batch writes
const widths = boxes.map(box => box.offsetWidth)  // Read all
boxes.forEach((box, i) => {
  box.style.width = (widths[i] + 10) + 'px'       // Write all
})
Properties that trigger layout when read:
PropertyWhat It Returns
offsetWidth / offsetHeightElement’s layout width/height including borders
offsetTop / offsetLeftPosition relative to offset parent
clientWidth / clientHeightInner dimensions (padding but no border)
scrollWidth / scrollHeightFull scrollable dimensions
scrollTop / scrollLeftCurrent scroll position
getBoundingClientRect()Position and size relative to viewport
getComputedStyle()All computed CSS values
// Any of these reads forces a layout calculation
const width = element.offsetWidth      // Layout triggered!
const rect = element.getBoundingClientRect()  // Layout triggered!
const styles = getComputedStyle(element)      // Layout triggered!

Use requestAnimationFrame for Visual Changes

Use requestAnimationFrame() to batch visual changes with the browser’s render cycle:
// Bad: DOM changes at unpredictable times
window.addEventListener('scroll', () => {
  element.style.transform = `translateY(${window.scrollY}px)`
})

// Good: Batch visual changes with next frame
let ticking = false
window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      element.style.transform = `translateY(${window.scrollY}px)`
      ticking = false
    })
    ticking = true
  }
})

The #1 DOM Mistake: Using innerHTML with User Input

The most dangerous DOM mistake is using innerHTML with untrusted content. This opens your application to Cross-Site Scripting (XSS) attacks.
┌─────────────────────────────────────────────────────────────────────────┐
│                    innerHTML: THE SECURITY TRAP                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ❌ DANGEROUS                              ✓ SAFE                        │
│  ─────────────                             ──────                        │
│                                                                          │
│  User Input:                               User Input:                   │
│  "<img src=x onerror=alert('XSS')>"       "<img src=x onerror=...>"     │
│         │                                        │                       │
│         ▼                                        ▼                       │
│  element.innerHTML = userInput             element.textContent = input   │
│         │                                        │                       │
│         ▼                                        ▼                       │
│  ┌─────────────────┐                      ┌─────────────────┐            │
│  │ BROWSER PARSES  │                      │ DISPLAYED AS    │            │
│  │ AS REAL HTML!   │                      │ PLAIN TEXT      │            │
│  │                 │                      │                 │            │
│  │ 🚨 Script runs! │                      │ "<img src=..."  │            │
│  │ Cookies stolen! │                      │ (harmless)      │            │
│  └─────────────────┘                      └─────────────────┘            │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
// ❌ DANGEROUS - Never do this with user input!
const username = getUserInput()  // User enters: <img src=x onerror="stealCookies()">
div.innerHTML = `Welcome, ${username}!`
// The malicious script EXECUTES!

// ✓ SAFE - textContent escapes HTML
const username = getUserInput()
div.textContent = `Welcome, ${username}!`
// Displays: Welcome, <img src=x onerror="stealCookies()">!
// The HTML is shown as text, not executed

// ✓ SAFE - Create elements programmatically
const username = getUserInput()
const welcomeText = document.createTextNode(`Welcome, ${username}!`)
div.appendChild(welcomeText)
The Trap: innerHTML looks convenient, but it parses strings as real HTML. If that string contains user input, attackers can inject <script> tags, malicious event handlers, or other dangerous code. Always use textContent for user-provided content.

Other Common Mistakes

// ❌ WRONG - Crashes if element doesn't exist
document.querySelector('.maybe-missing').classList.add('active')
// TypeError: Cannot read property 'classList' of null

// ✓ CORRECT - Check first or use optional chaining
const element = document.querySelector('.maybe-missing')
if (element) {
  element.classList.add('active')
}

// Or use optional chaining (modern)
document.querySelector('.maybe-missing')?.classList.add('active')
// ❌ CONFUSING - Includes whitespace text nodes!
const ul = document.querySelector('ul')
console.log(ul.childNodes.length)  // 7 (includes text nodes!)

// ✓ CLEAR - Only element children
console.log(ul.children.length)  // 3 (just the <li> elements)
// ❌ SLOW - Forces layout on every iteration
boxes.forEach(box => {
  const width = box.offsetWidth      // READ - forces layout
  box.style.width = width + 10 + 'px' // WRITE - invalidates layout
})

// ✓ FAST - Batch reads, then batch writes
const widths = boxes.map(box => box.offsetWidth)  // All reads
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px'          // All writes
})

Event Propagation: Bubbling and Capturing

When an event occurs on a DOM element, it doesn’t just trigger on that element. It travels through the DOM tree in a process called event propagation. Understanding this helps with event handling.

The Three Phases

Every DOM event goes through three phases:
1. CAPTURING PHASE    ↓  (from window → target's parent)
2. TARGET PHASE       ●  (at the target element)
3. BUBBLING PHASE     ↑  (from target's parent → window)
// Most events bubble UP by default
document.querySelector('.child').addEventListener('click', (e) => {
  console.log('Child clicked')
})

document.querySelector('.parent').addEventListener('click', (e) => {
  console.log('Parent also receives the click!')  // This fires too!
})

Capturing vs Bubbling

By default, event listeners fire during the bubbling phase (bottom-up). You can listen during the capturing phase (top-down) with the third parameter:
// Bubbling (default) — fires on the way UP
element.addEventListener('click', handler)
element.addEventListener('click', handler, false)

// Capturing — fires on the way DOWN
element.addEventListener('click', handler, true)
element.addEventListener('click', handler, { capture: true })
// Practical example: see the order
document.querySelector('.parent').addEventListener('click', () => {
  console.log('1. Parent - capturing')
}, true)

document.querySelector('.child').addEventListener('click', () => {
  console.log('2. Child - target')
})

document.querySelector('.parent').addEventListener('click', () => {
  console.log('3. Parent - bubbling')
})

// Click on child outputs: 1, 2, 3

Stopping Propagation

You can stop an event from traveling further:
element.addEventListener('click', (e) => {
  e.stopPropagation()        // Stop bubbling/capturing
  // Parent handlers won't fire
})

element.addEventListener('click', (e) => {
  e.stopImmediatePropagation()  // Stop ALL handlers, even on same element
})
Use stopPropagation() sparingly! It breaks event delegation and can make debugging difficult. Usually there’s a better solution.

Preventing Default Behavior

Don’t confuse propagation with default behavior:
// Prevent the browser's default action (e.g., following a link)
link.addEventListener('click', (e) => {
  e.preventDefault()   // Don't navigate
  // Event still bubbles unless you also call stopPropagation()
})

// Common use cases:
// - Prevent form submission: form.addEventListener('submit', e => e.preventDefault())
// - Prevent link navigation: link.addEventListener('click', e => e.preventDefault())
// - Prevent context menu: element.addEventListener('contextmenu', e => e.preventDefault())

The event.target vs event.currentTarget

This distinction matters for event delegation:
document.querySelector('.parent').addEventListener('click', (e) => {
  console.log(e.target)        // The element that was actually clicked
  console.log(e.currentTarget) // The element with the listener (.parent)
  console.log(this)            // Same as currentTarget (in regular functions)
})
// If you click on a <span> inside .parent:
// e.target = <span>           (what you clicked)
// e.currentTarget = .parent   (what has the listener)

Events That Don’t Bubble

Most events bubble, but some don’t:
EventBubbles?Notes
click, mousedown, keydownYesMost user events bubble
focus, blurNoUse focusin/focusout for bubbling versions
mouseenter, mouseleaveNoUse mouseover/mouseout for bubbling versions
load, unload, scrollNoWindow/document events
// focus doesn't bubble, but focusin does
form.addEventListener('focusin', (e) => {
  console.log('Something in the form was focused:', e.target)
})

Common DOM Patterns

Event Delegation

Instead of adding listeners to many elements, add one to a parent. This pattern relies on event bubbling. When you click a child element, the event bubbles up to the parent where your listener catches it:
// Bad: Many listeners
document.querySelectorAll('.btn').forEach(btn => {
  btn.addEventListener('click', handleClick)
})

// Good: One listener with delegation
document.querySelector('.button-container').addEventListener('click', (e) => {
  const btn = e.target.closest('.btn')
  if (btn) {
    handleClick(e)
  }
})
Benefits:
  • Works for dynamically added elements
  • Less memory usage
  • Easier cleanup (uses closures to maintain handler references)

Checking if Element Exists

// Using querySelector (returns null if not found)
const element = document.querySelector('.maybe-exists')
if (element) {
  element.textContent = 'Found!'
}

// Optional chaining (modern)
document.querySelector('.maybe-exists')?.classList.add('active')

// With getElementById
const el = document.getElementById('myId')
if (el !== null) {
  // Element exists
}

Waiting for DOM Ready

Listen for the DOMContentLoaded event to know when the DOM is ready:
// Modern: DOMContentLoaded (DOM ready, images may still be loading)
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM is ready!')
  // Safe to query elements
})

// Full page load (including images, stylesheets)
window.addEventListener('load', () => {
  console.log('Everything loaded!')
})

// If script is at end of body, DOM is already ready
// <script src="app.js"></script> <!-- Just before </body> -->

// Modern: defer attribute (script loads in parallel, runs after DOM ready)
// <script src="app.js" defer></script>
Best practice: Put your <script> tags just before </body> or use the defer attribute. Then you don’t need to wait for DOMContentLoaded.

Common Misconceptions

Wrong! The DOM is NOT your HTML file. The browser:
  1. Fixes errors — Missing <head>, <body>, unclosed tags are auto-corrected
  2. Normalizes structure — Text outside elements gets wrapped properly
  3. Reflects JavaScript changes — DOM updates don’t change your HTML file
<!-- Your HTML file -->
<html>Hello World

<!-- What the DOM looks like -->
<html>
  <head></head>
  <body>Hello World</body>
</html>
View Source shows your file. DevTools Elements shows the DOM.
Mostly wrong! Yes, getElementById is technically faster (O(1) hashtable lookup), but:
  • The difference is microseconds — imperceptible to users
  • querySelector is more flexible and readable
  • You’d need to call it thousands of times in a loop to notice
// Both are fine for normal use
document.getElementById('myId')
document.querySelector('#myId')

// Only optimize if you're selecting in a tight loop
// with performance issues (rare!)
Rule: Write readable code first. Optimize only when you have a measured problem.
Wrong! display: none hides the element visually, but it’s still in the DOM:
element.style.display = 'none'

// Element is STILL in the DOM!
console.log(document.getElementById('hidden'))  // Element exists
console.log(element.parentNode)                 // Still has parent

// To actually remove from DOM:
element.remove()
// or
element.parentNode.removeChild(element)
  • display: none → Hidden but in DOM, not in Render Tree
  • visibility: hidden → Hidden but takes up space, in Render Tree
  • remove() → Actually removed from DOM
Misleading! Live collections (getElementsByClassName, getElementsByTagName) update automatically, but this can cause bugs:
const items = document.getElementsByClassName('item')

// DANGER: Removing items changes the collection while looping!
for (let i = 0; i < items.length; i++) {
  items[i].remove()  // Collection shrinks, indices shift!
}
// Some items are skipped!

// SAFE: Use static NodeList or convert to array
const items = document.querySelectorAll('.item')  // Static
items.forEach(item => item.remove())  // Works correctly
Tip: Prefer querySelectorAll (static) unless you specifically need live updates.

Classic Interview Questions

Question 1: What’s the difference between document.querySelector and document.getElementById?

FeaturegetElementByIdquerySelector
Selector typeID onlyAny CSS selector
ReturnsElement or nullElement or null
SpeedFaster (hashtable)Slightly slower (parses CSS)
FlexibilityLowHigh
// getElementById — only IDs
document.getElementById('myId')

// querySelector — any CSS selector
document.querySelector('#myId')           // Same as above
document.querySelector('.card:first-child')  // Not possible with getElementById
document.querySelector('[data-id="123"]')    // Attribute selector
Best answer: “getElementById is marginally faster but querySelector is more flexible. In practice, the performance difference is negligible for most applications. I prefer querySelector for consistency and flexibility.”

Question 2: Explain event delegation and why it’s useful

Event delegation is attaching a single event listener to a parent element instead of multiple listeners to child elements. It works because events “bubble up” the DOM tree.
// ❌ Without delegation — 100 listeners for 100 items
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', handleClick)
})

// ✓ With delegation — 1 listener handles all items
document.querySelector('.container').addEventListener('click', (e) => {
  const item = e.target.closest('.item')
  if (item) handleClick(e)
})
Benefits:
  1. Memory efficient — One listener vs. many
  2. Works for dynamic elements — New items automatically handled
  3. Easier cleanup — Remove one listener to clean up
Best answer: Include a code example and mention closest() for finding the target element.

Question 3: What causes layout thrashing and how do you avoid it?

Layout thrashing occurs when you repeatedly alternate between reading and writing DOM layout properties, forcing the browser to recalculate layout multiple times.
// ❌ Thrashing — forces layout on EVERY iteration
boxes.forEach(box => {
  const width = box.offsetWidth     // READ → triggers layout
  box.style.width = width + 10 + 'px' // WRITE → invalidates layout
})

// ✓ Batched — one layout calculation
const widths = boxes.map(box => box.offsetWidth)  // All reads
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px'          // All writes
})
Properties that trigger layout: offsetWidth/Height, clientWidth/Height, getBoundingClientRect(), getComputedStyle()Best answer: Explain the read-write-read-write pattern and show the batched solution.

Question 4: What’s the difference between innerHTML, textContent, and innerText?

PropertyParses HTML?Includes hidden text?PerformanceSecurity
innerHTMLYesYesSlowerXSS risk
textContentNoYesFastSafe
innerTextNoNo (respects CSS)SlowestSafe
// <div id="el"><span style="display:none">Hidden</span> Visible</div>

el.innerHTML     // "<span style="display:none">Hidden</span> Visible"
el.textContent   // "Hidden Visible"
el.innerText     // " Visible" (hidden text excluded)
Security warning: Never use innerHTML with user input. It can execute malicious scripts (XSS attacks). Use textContent instead.Best answer: Mention the XSS security risk with innerHTML. This shows you understand real-world implications.

Question 5: How do you efficiently add 1000 elements to the DOM?

Use a DocumentFragment to batch insertions:
// ❌ Slow — 1000 DOM updates, 1000 potential reflows
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  ul.appendChild(li)  // Triggers update each time
}

// ✓ Fast — 1 DOM update
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  fragment.appendChild(li)  // No DOM update (fragment is detached)
}
ul.appendChild(fragment)  // Single update
Alternative: Build an HTML string and use innerHTML once (but only with trusted content, never user input).Best answer: Show the fragment approach and explain WHY it’s faster (detached container, single reflow).

Question 6: What’s the difference between attributes and properties?

Attributes are defined in HTML. Properties are the live state on DOM objects.
<input type="text" value="initial">
const input = document.querySelector('input')

// Attribute — original HTML value
input.getAttribute('value')  // "initial" (never changes)

// Property — current live value
input.value  // "initial" initially, then whatever user types

// User types "hello"...
input.getAttribute('value')  // Still "initial"
input.value                  // "hello"
AspectAttributeProperty
SourceHTML markupDOM object
TypeAlways stringCan be any type
UpdatesManual onlyAutomatically with interaction
Best answer: Use the <input value=""> example. It’s the clearest demonstration of the difference.

Key Takeaways

The key things to remember:
  1. The DOM is a tree — Elements are nodes with parent, child, and sibling relationships
  2. DOM ≠ HTML source — The browser fixes errors and JavaScript modifies it
  3. Use querySelector — More flexible than getElementById, accepts any CSS selector
  4. Element vs Node properties — Use children, firstElementChild, etc. to skip text nodes
  5. closest() is your friend — Perfect for event delegation and finding ancestor elements
  6. innerHTML is dangerous — Never use with user input; use textContent instead
  7. Attributes vs Properties — Attributes are HTML source, properties are live DOM state
  8. classList over className — Use add/remove/toggle for cleaner class manipulation
  9. Batch DOM operations — Use DocumentFragment or build strings to minimize reflows
  10. Avoid layout thrashing — Don’t alternate reading and writing layout properties

Test Your Knowledge

Answer:
  • childNodes returns ALL child nodes, including text nodes (whitespace!) and comment nodes
  • children returns only element nodes
// <ul>
//   <li>One</li>
//   <li>Two</li>
// </ul>

ul.childNodes.length  // 5 (text, li, text, li, text)
ul.children.length    // 2 (li, li)
Rule: Use children unless you specifically need text/comment nodes.
Answer: innerHTML parses strings as HTML, enabling Cross-Site Scripting (XSS) attacks:
// User input: <img src=x onerror="stealCookies()">
div.innerHTML = userInput  // Executes malicious code!

// Safe: textContent escapes HTML
div.textContent = userInput  // Displays as plain text
Always sanitize HTML or use textContent for user-provided content.
Answer:
  • getAttribute('value') returns the original HTML attribute (initial value)
  • .value property returns the current value (what user typed)
// <input value="initial">
// User types "hello"

input.getAttribute('value')  // "initial"
input.value                  // "hello"
Attributes are the HTML source. Properties are the live DOM state.
Answer: closest() finds the nearest ancestor (including the element itself) that matches a selector:
// <div class="card">
//   <button class="btn">Click</button>
// </div>

btn.closest('.card')  // Returns the parent div
btn.closest('button') // Returns btn itself (it matches!)
btn.closest('.modal') // null (no matching ancestor)
Super useful for event delegation:
document.addEventListener('click', (e) => {
  const card = e.target.closest('.card')
  if (card) {
    // Handle click inside any card
  }
})
Answer: Layout thrashing happens when you alternate reading and writing layout-triggering properties:
// BAD: Read-write-read-write pattern
boxes.forEach(box => {
  const width = box.offsetWidth     // READ → forces layout
  box.style.width = width + 10 + 'px' // WRITE → invalidates layout
})
// Each iteration forces a new layout calculation!

// GOOD: Batch reads, then batch writes
const widths = boxes.map(b => b.offsetWidth)  // All reads
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px'     // All writes
})
// Only one layout calculation!
Answer: The DOM contains all nodes from the HTML (plus JS modifications). The Render Tree contains only visible elements with their computed styles.In DOM but NOT in Render Tree:
  • <head> and its contents
  • <script>, <link>, <meta> tags
  • Elements with display: none
In Render Tree:
  • Visible elements
  • Elements with visibility: hidden (still take space)
  • Elements with opacity: 0 (still take space)
Pseudo-elements (::before, ::after) are in the Render Tree but NOT in the DOM.
Answer:
AspectgetElementsByClassNamequerySelectorAll
ReturnsHTMLCollectionNodeList
LiveYes (updates automatically)No (static snapshot)
SelectorClass name onlyAny CSS selector
SpeedSlightly fasterSlightly slower
const live = document.getElementsByClassName('item')
const staticList = document.querySelectorAll('.item')

// Add new element with class="item"
document.body.appendChild(newItem)

live.length       // Increased (live collection)
staticList.length // Same (static snapshot)
Answer: Use a DocumentFragment to batch insertions:
const fragment = document.createDocumentFragment()

for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  fragment.appendChild(li)  // No reflow (fragment is detached)
}

ul.appendChild(fragment)  // Single reflow!
A DocumentFragment is a virtual container. When appended, only its children are inserted. The fragment disappears.Alternative: Build HTML string and use innerHTML once (but sanitize if user input!).

Frequently Asked Questions

The DOM (Document Object Model) is a programming interface that represents an HTML document as a tree of objects. As defined by the WHATWG DOM Living Standard, it provides a structured representation that JavaScript can read and modify. Every element, attribute, and text node becomes an object in this tree, allowing dynamic page manipulation.
HTML is the static markup you write in a file. The DOM is the live, in-memory representation the browser creates after parsing that HTML. The browser corrects errors (adding missing tags, fixing nesting), executes JavaScript that modifies it, and keeps it in sync with what you see on screen. The DOM can differ significantly from your original HTML source.
getElementById finds a single element by its id attribute and is the fastest DOM lookup method. querySelector accepts any CSS selector and is more flexible, but slightly slower. According to MDN, getElementById is only available on the document object, while querySelector can be called on any element to search within its subtree.
Layout thrashing occurs when you repeatedly read layout properties (like offsetHeight) and then write to the DOM in the same synchronous block. Each read forces the browser to recalculate layout before returning a value, and each write invalidates that layout. According to Google’s web performance research, layout thrashing is one of the most common causes of janky scrolling and slow interactions.
Event delegation attaches a single event listener to a parent element instead of adding listeners to every child. It works because DOM events bubble up from the target through ancestor elements. You check event.target to identify which child was clicked. This pattern is more memory-efficient and works automatically for dynamically added elements.


Reference

Articles

Eloquent JavaScript: The Document Object Model

A free book chapter with runnable code examples you can edit right in the browser. Includes exercises at the end to test your understanding.

How To Understand and Modify the DOM in JavaScript

Tania Rascia walks through each concept with side-by-side HTML and JavaScript examples. Great for visual learners who want to see code and results together.

What's the Document Object Model, and why you should know how to use it

Builds a simple project while explaining DOM concepts. Good if you learn better by building something rather than reading theory.

What is the DOM?

Short read that clears up the “DOM vs HTML source” confusion with visual examples. Explains why DevTools shows something different from View Source.

Traversing the DOM with JavaScript

Zell explains the difference between Node and Element traversal methods with clear diagrams. Includes the “whitespace text node” gotcha that trips up beginners.

DOM Tree

Interactive examples you can edit and run in the browser. Part of a larger DOM tutorial series if you want to keep going deeper.

How to traverse the DOM in JavaScript

Covers every traversal method with console output screenshots. Useful reference when you forget which property to use for siblings vs children.

Render Tree Construction

Google’s official explanation of the Critical Rendering Path. Essential reading if you want to understand why some DOM operations are slow.

What, exactly, is the DOM?

Compares DOM vs HTML source vs Render Tree side by side with diagrams. Clears up the confusion about what DevTools actually shows you.

JavaScript DOM Tutorial

A multi-part tutorial organized by topic, so you can jump to exactly what you need. Each page is self-contained with try-it-yourself examples.

Event Propagation — MDN

MDN’s guide to event handling including bubbling, capturing, and delegation patterns.

Bubbling and Capturing

Animated diagrams showing events traveling up and down the DOM tree. Makes the three-phase model (capture, target, bubble) easy to visualize.

Videos

Last modified on February 17, 2026