Learn the DOM in JavaScript. Select and manipulate elements, traverse nodes, handle events, and optimize rendering performance.
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?
Copy
Ask AI
// 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!
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.
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.
Copy
Ask AI
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:
Action
Family Analogy
DOM 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.
Here’s the key thing: your HTML file and the DOM are different things:
HTML Source
Resulting DOM
Copy
Ask AI
<!-- What you wrote (invalid HTML - missing head/body) --><!DOCTYPE html><html>Hello, World!</html>
Copy
Ask AI
<!-- What the browser creates (fixed!) --><!DOCTYPE html><html> <head></head> <body> Hello, World! </body></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 Render Tree is what actually gets painted to the screen. It excludes:
Copy
Ask AI
<!-- 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 -->
// document is the root of everythingconsole.log(document) // The entire documentconsole.log(document.documentElement) // <html> elementconsole.log(document.head) // <head> elementconsole.log(document.body) // <body> elementconsole.log(document.title) // Page title (getter/setter!)// You can modify the documentdocument.title = 'New Title' // Changes browser tab title
Instead of remembering numbers, use the constants:
Copy
Ask AI
Node.ELEMENT_NODE // 1Node.TEXT_NODE // 3Node.COMMENT_NODE // 8Node.DOCUMENT_NODE // 9Node.DOCUMENT_FRAGMENT_NODE // 11// Check if something is an elementif (node.nodeType === Node.ELEMENT_NODE) { console.log('This is an element!')}
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.
// querySelector returns the FIRST match (or null)const firstButton = document.querySelector('button') // First <button> elementconst 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> elementsconst allCards = document.querySelectorAll('.card') // All elements with class="card"const evenRows = document.querySelectorAll('tr:nth-child(even)') // Every even table row
// By IDdocument.querySelector('#main')// By classdocument.querySelector('.active')document.querySelectorAll('.btn.primary')// By tagdocument.querySelector('header')document.querySelectorAll('li')// By attributedocument.querySelector('[type="submit"]')document.querySelector('[data-modal="login"]')// Descendant selectorsdocument.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')
You can call selection methods on any element, not just document:
Copy
Ask AI
const nav = document.querySelector('nav')// Find links ONLY inside navconst navLinks = nav.querySelectorAll('a')// Find the active link inside navconst activeLink = nav.querySelector('.active')
This is faster than searching the entire document and helps avoid selecting unintended elements.
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.
Copy
Ask AI
// Premature optimization - don't do thisconst el1 = document.getElementById('myId')// This is fine and more readableconst el2 = document.querySelector('#myId')
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 childrenconst firstChild = ul.firstChild // First node (might be text!)const firstElement = ul.firstElementChild // First ELEMENT childconst lastChild = ul.lastChild // Last nodeconst lastElement = ul.lastElementChild // Last ELEMENT child
The Text Node Trap! Look at this HTML:
Copy
Ask AI
<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.
const li = document.querySelector('li')// Direct parentconst parent = li.parentNode // Usually same as parentElementconst 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 itselfconst 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):
Copy
Ask AI
// Handle clicks on any button inside a carddocument.addEventListener('click', (e) => { const card = e.target.closest('.card') if (card) { console.log('Clicked inside card:', card) }})
// Get all ancestors of an elementfunction 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>, ...]
// Create a new elementconst div = document.createElement('div')const span = document.createElement('span')const img = document.createElement('img')// Create a text nodeconst text = document.createTextNode('Hello, world!')// Create a comment nodeconst comment = document.createComment('This is a comment')// Elements are created "detached" - not yet in the DOM!console.log(div.parentNode) // null
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>
Modern methods that accept multiple nodes AND strings:
Copy
Ask AI
const div = document.querySelector('div')// append() - adds to the ENDdiv.append('Text', document.createElement('span'), 'More text')// prepend() - adds to the STARTdiv.prepend(document.createElement('strong'))
Insert as siblings (not children):
Copy
Ask AI
const h1 = document.querySelector('h1')// Insert BEFORE h1 (as previous sibling)h1.before(document.createElement('nav'))// Insert AFTER h1 (as next sibling)h1.after(document.createElement('p'))
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>')
const element = document.querySelector('.to-remove')element.remove() // Gone!
Classic method. Remove via parent:
Copy
Ask AI
const parent = document.querySelector('ul')const child = parent.querySelector('li')parent.removeChild(child)// Or remove from any elementelement.parentNode.removeChild(element)
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 DOMdocument.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:
When adding many elements, using a DocumentFragment is more efficient:
Copy
Ask AI
// 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 updateconst 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.
const div = document.querySelector('div')// Read HTML contentconsole.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 contentdiv.innerHTML = ''
Security Alert: XSS Vulnerability!Never use innerHTML with user-provided content:
Copy
Ask AI
// 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
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!
Getting text as user sees it (slower, respects CSS)
Copy
Ask AI
// Performance: textContent is faster than innerText// because innerText must calculate styles// Setting text content (both work, textContent is faster)element.textContent = 'Hello' // Preferredelement.innerText = 'Hello' // Works but slower
This confuses many developers! Attributes are in the HTML. Properties are on the DOM object.
Copy
Ask AI
<input type="text" value="initial">
Copy
Ask AI
const input = document.querySelector('input')// ATTRIBUTE: The original HTML valueconsole.log(input.getAttribute('value')) // "initial"// PROPERTY: The current stateconsole.log(input.value) // "initial"// User types "new text"...console.log(input.getAttribute('value')) // Still "initial"!console.log(input.value) // "new text"// Reset to attribute valueinput.value = input.getAttribute('value')
Key differences:
Aspect
Attribute
Property
Source
HTML markup
DOM object
Access
get/setAttribute()
Direct property access
Updates
Manual only
Automatically with user interaction
Type
Always string
Can be any type
Copy
Ask AI
// Attribute is always a stringcheckbox.getAttribute('checked') // "" or null// Property is a booleancheckbox.checked // true or false// Attribute (string)input.getAttribute('maxlength') // "10"// Property (number)input.maxLength // 10
Custom data attributes start with data- and are accessible via the dataset property:
Copy
Ask AI
<div id="user" data-user-id="123" data-role="admin" data-is-active="true"> John Doe</div>
Copy
Ask AI
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 attributesuser.dataset.lastLogin = '2024-01-15'// Creates: data-last-login="2024-01-15"// Delete data attributesdelete user.dataset.role// Check if existsif ('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!
// 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)
The classList API is the modern way to add/remove/toggle classes:
Copy
Ask AI
const button = document.querySelector('button')// Add classesbutton.classList.add('active')button.classList.add('btn', 'btn-primary') // Multiple at once// Remove classesbutton.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 conditionbutton.classList.toggle('active', isActive) // Add if isActive is true// Check if class existsif (button.classList.contains('active')) { console.log('Button is active')}// Replace a classbutton.classList.replace('btn-primary', 'btn-secondary')// Iterate over classesbutton.classList.forEach(cls => console.log(cls))// Get number of classesconsole.log(button.classList.length) // 2
// className is a string (old way)element.className = 'btn btn-primary' // Replaces ALL classeselement.className += ' active' // Appending is clunky// classList is a DOMTokenList (modern way)element.classList.add('active') // Adds without affecting otherselement.classList.remove('btn-primary') // Removes specifically
Understanding how browsers render pages helps you write performant code. This is where JavaScript Engines and the browser’s rendering engine work together.
<!-- 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 -->
Modern browsers separate content into layers and use the GPU to composite them. This is why some animations are smooth:
Copy
Ask AI
/* 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! */
// Bad: Queries the DOM every iterationfor (let i = 0; i < 1000; i++) { document.querySelector('.result').textContent += i}// Good: Query once, reuseconst result = document.querySelector('.result')for (let i = 0; i < 1000; i++) { result.textContent += i}// Even better: Build string, set onceconst result = document.querySelector('.result')let text = ''for (let i = 0; i < 1000; i++) { text += i}result.textContent = text
// 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 cssTextelement.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 elementsconst 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.
// ❌ 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 HTMLconst username = getUserInput()div.textContent = `Welcome, ${username}!`// Displays: Welcome, <img src=x onerror="stealCookies()">!// The HTML is shown as text, not executed// ✓ SAFE - Create elements programmaticallyconst 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.
// ❌ WRONG - Crashes if element doesn't existdocument.querySelector('.maybe-missing').classList.add('active')// TypeError: Cannot read property 'classList' of null// ✓ CORRECT - Check first or use optional chainingconst element = document.querySelector('.maybe-missing')if (element) { element.classList.add('active')}// Or use optional chaining (modern)document.querySelector('.maybe-missing')?.classList.add('active')
Using childNodes instead of children
Copy
Ask AI
// ❌ CONFUSING - Includes whitespace text nodes!const ul = document.querySelector('ul')console.log(ul.childNodes.length) // 7 (includes text nodes!)// ✓ CLEAR - Only element childrenconsole.log(ul.children.length) // 3 (just the <li> elements)
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.
// Most events bubble UP by defaultdocument.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!})
By default, event listeners fire during the bubbling phase (bottom-up). You can listen during the capturing phase (top-down) with the third parameter:
Copy
Ask AI
// Bubbling (default) — fires on the way UPelement.addEventListener('click', handler)element.addEventListener('click', handler, false)// Capturing — fires on the way DOWNelement.addEventListener('click', handler, true)element.addEventListener('click', handler, { capture: true })
// 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())
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)})
Copy
Ask AI
// If you click on a <span> inside .parent:// e.target = <span> (what you clicked)// e.currentTarget = .parent (what has the listener)
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:
Copy
Ask AI
// Bad: Many listenersdocument.querySelectorAll('.btn').forEach(btn => { btn.addEventListener('click', handleClick)})// Good: One listener with delegationdocument.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)
// 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 getElementByIdconst el = document.getElementById('myId')if (el !== null) { // Element exists}
Listen for the DOMContentLoaded event to know when the DOM is ready:
Copy
Ask AI
// 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.
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
Copy
Ask AI
// Both are fine for normal usedocument.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.
Misconception 3: 'display: none removes the element from the DOM'
Wrong!display: none hides the element visually, but it’s still in the DOM:
Copy
Ask AI
element.style.display = 'none'// Element is STILL in the DOM!console.log(document.getElementById('hidden')) // Element existsconsole.log(element.parentNode) // Still has parent// To actually remove from DOM:element.remove()// orelement.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
Misconception 4: 'Live collections automatically update my code'
Misleading! Live collections (getElementsByClassName, getElementsByTagName) update automatically, but this can cause bugs:
Copy
Ask AI
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 arrayconst items = document.querySelectorAll('.item') // Staticitems.forEach(item => item.remove()) // Works correctly
Tip: Prefer querySelectorAll (static) unless you specifically need live updates.
Question 1: What’s the difference between document.querySelector and document.getElementById?
Answer
Feature
getElementById
querySelector
Selector type
ID only
Any CSS selector
Returns
Element or null
Element or null
Speed
Faster (hashtable)
Slightly slower (parses CSS)
Flexibility
Low
High
Copy
Ask AI
// getElementById — only IDsdocument.getElementById('myId')// querySelector — any CSS selectordocument.querySelector('#myId') // Same as abovedocument.querySelector('.card:first-child') // Not possible with getElementByIddocument.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
Answer
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.
Copy
Ask AI
// ❌ Without delegation — 100 listeners for 100 itemsdocument.querySelectorAll('.item').forEach(item => { item.addEventListener('click', handleClick)})// ✓ With delegation — 1 listener handles all itemsdocument.querySelector('.container').addEventListener('click', (e) => { const item = e.target.closest('.item') if (item) handleClick(e)})
Benefits:
Memory efficient — One listener vs. many
Works for dynamic elements — New items automatically handled
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?
Answer
Layout thrashing occurs when you repeatedly alternate between reading and writing DOM layout properties, forcing the browser to recalculate layout multiple times.
Properties that trigger layout:offsetWidth/Height, clientWidth/Height, getBoundingClientRect(), getComputedStyle()Best answer: Explain the read-write-read-write pattern and show the batched solution.
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?
Answer
Use a DocumentFragment to batch insertions:
Copy
Ask AI
// ❌ Slow — 1000 DOM updates, 1000 potential reflowsfor (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 updateconst 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?
Answer
Attributes are defined in HTML. Properties are the live state on DOM objects.
Copy
Ask AI
<input type="text" value="initial">
Copy
Ask AI
const input = document.querySelector('input')// Attribute — original HTML valueinput.getAttribute('value') // "initial" (never changes)// Property — current live valueinput.value // "initial" initially, then whatever user types// User types "hello"...input.getAttribute('value') // Still "initial"input.value // "hello"
Aspect
Attribute
Property
Source
HTML markup
DOM object
Type
Always string
Can be any type
Updates
Manual only
Automatically with interaction
Best answer: Use the <input value=""> example. It’s the clearest demonstration of the difference.
document.addEventListener('click', (e) => { const card = e.target.closest('.card') if (card) { // Handle click inside any card }})
Question 5: What causes layout thrashing and how do you avoid it?
Answer: Layout thrashing happens when you alternate reading and writing layout-triggering properties:
Copy
Ask AI
// BAD: Read-write-read-write patternboxes.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 writesconst widths = boxes.map(b => b.offsetWidth) // All readsboxes.forEach((box, i) => { box.style.width = widths[i] + 10 + 'px' // All writes})// Only one layout calculation!
Question 6: What's in the Render Tree vs the DOM?
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.
Question 7: getElementsByClassName vs querySelectorAll - what's different?
Answer:
Aspect
getElementsByClassName
querySelectorAll
Returns
HTMLCollection
NodeList
Live
Yes (updates automatically)
No (static snapshot)
Selector
Class name only
Any CSS selector
Speed
Slightly faster
Slightly slower
Copy
Ask AI
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)
Question 8: How do you safely add many elements to the DOM?
Answer: Use a DocumentFragment to batch insertions:
Copy
Ask AI
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!).
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.
What is the difference between the DOM and HTML?
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.
What is the difference between getElementById and querySelector?
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.
What causes layout thrashing in the DOM?
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.
How does event delegation work in JavaScript?
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.