JavaScript & the DOM
JavaScript brings web pages to life by enabling dynamic behavior, user interaction, and real-time updates. At its core, JavaScript interacts with the web page through the DOM (Document Object Model) — a tree-structured representation of the HTML document that JavaScript can read and modify.
The DOM Tree Structure
When a browser parses an HTML document, it constructs a tree of nodes called the DOM. Each HTML element, text content, comment, and attribute becomes a node in this tree.
document└── html ├── head │ ├── title │ │ └── "My Page" (text node) │ └── meta └── body ├── header │ └── nav │ └── ul │ ├── li │ │ └── a │ │ └── "Home" (text node) │ └── li │ └── a │ └── "About" (text node) ├── main │ ├── h1 │ │ └── "Welcome" (text node) │ └── p │ └── "Hello world" (text node) └── footer └── p └── "© 2025" (text node)Node Types
| Node Type | nodeType Value | Example |
|---|---|---|
| Element | 1 | <div>, <p>, <a> |
| Text | 3 | "Hello world" |
| Comment | 8 | <!-- comment --> |
| Document | 9 | document |
| DocumentFragment | 11 | document.createDocumentFragment() |
Selecting Elements
JavaScript provides multiple methods to locate DOM elements:
// By ID (returns single element or null)const header = document.getElementById('main-header');
// By CSS selector (returns first match or null)const firstCard = document.querySelector('.card');const navLink = document.querySelector('nav a[href="/about"]');
// By CSS selector (returns NodeList of ALL matches)const allCards = document.querySelectorAll('.card');const allLinks = document.querySelectorAll('a[target="_blank"]');
// By class name (returns live HTMLCollection)const buttons = document.getElementsByClassName('btn');
// By tag name (returns live HTMLCollection)const paragraphs = document.getElementsByTagName('p');
// Traversing the treeconst parent = element.parentElement;const children = element.children; // HTMLCollection (elements only)const childNodes = element.childNodes; // NodeList (includes text nodes)const firstChild = element.firstElementChild;const lastChild = element.lastElementChild;const nextSib = element.nextElementSibling;const prevSib = element.previousElementSibling;const closest = element.closest('.container'); // walk up the treeIterating Over NodeLists
// querySelectorAll returns a NodeList (iterable, but not an Array)const cards = document.querySelectorAll('.card');
// forEach works on NodeListcards.forEach(card => console.log(card.textContent));
// Convert to Array for full Array methodsconst cardArray = Array.from(cards);// or: const cardArray = [...cards];
cardArray.filter(card => card.classList.contains('featured')) .map(card => card.dataset.title);DOM Manipulation
Creating and Inserting Elements
// Create elementsconst article = document.createElement('article');const heading = document.createElement('h2');const paragraph = document.createElement('p');
// Set content and attributesheading.textContent = 'New Article'; // text only (safe from XSS)paragraph.innerHTML = '<strong>Bold</strong> text'; // parses HTML (use carefully)article.setAttribute('id', 'article-1');article.classList.add('card', 'featured');article.dataset.category = 'tech'; // sets data-category="tech"
// Build the treearticle.appendChild(heading);article.appendChild(paragraph);
// Insert into the documentconst container = document.querySelector('.container');container.appendChild(article); // append as last childcontainer.prepend(article); // insert as first childcontainer.insertBefore(article, container.children[2]); // insert before 3rd child
// Modern insertion methodscontainer.append(article, anotherElement); // append multiple (accepts strings too)container.before(article); // insert before containercontainer.after(article); // insert after containerelement.replaceWith(article); // replace element
// Insert adjacent HTML/elementscontainer.insertAdjacentHTML('beforebegin', '<div>Before</div>');container.insertAdjacentHTML('afterbegin', '<div>First child</div>');container.insertAdjacentHTML('beforeend', '<div>Last child</div>');container.insertAdjacentHTML('afterend', '<div>After</div>');Modifying Elements
const el = document.querySelector('.card');
// Classesel.classList.add('active', 'highlighted');el.classList.remove('hidden');el.classList.toggle('expanded'); // add if absent, remove if presentel.classList.replace('old', 'new');el.classList.contains('active'); // returns boolean
// Styles (inline styles — use sparingly)el.style.backgroundColor = '#f0f0f0';el.style.setProperty('--custom-color', '#2563eb');el.style.cssText = 'color: red; font-size: 18px;'; // replaces all inline styles
// Attributesel.setAttribute('aria-expanded', 'true');el.getAttribute('data-id');el.removeAttribute('disabled');el.hasAttribute('required');
// Data attributesel.dataset.userId = '42'; // sets data-user-id="42"console.log(el.dataset.userId); // reads data-user-id
// Contentel.textContent = 'Safe text'; // does not parse HTMLel.innerHTML = '<em>Parsed HTML</em>'; // parses and renders HTMLel.outerHTML; // element + its HTML
// Removingel.remove(); // remove from DOMparent.removeChild(el); // older APIel.replaceChildren(); // remove all childrenPerformance: Document Fragments and Batch Updates
// BAD: causes reflow on every iterationconst list = document.querySelector('ul');for (let i = 0; i < 1000; i++) { const li = document.createElement('li'); li.textContent = `Item ${i}`; list.appendChild(li); // triggers layout recalculation each time}
// GOOD: batch with DocumentFragmentconst 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 in memory}list.appendChild(fragment); // single reflow when inserted
// ALSO GOOD: build HTML stringconst items = Array.from( { length: 1000 }, (_, i) => `<li>Item ${i}</li>`).join('');list.innerHTML = items; // single parse and renderEvent Handling
Events are the primary mechanism for user interaction in the browser.
addEventListener
const button = document.querySelector('#submit-btn');
// Add event listenerbutton.addEventListener('click', function(event) { console.log('Button clicked!'); console.log('Target:', event.target); // element that fired the event console.log('CurrentTarget:', event.currentTarget); // element listener is on console.log('Type:', event.type); // 'click' console.log('Timestamp:', event.timeStamp);});
// With optionsbutton.addEventListener('click', handler, { once: true, // automatically removes after first call passive: true, // promises not to call preventDefault (for scroll perf) capture: false, // use bubbling phase (default) signal: controller.signal // AbortController for cleanup});
// Remove event listener (must pass same function reference)function handleClick(event) { console.log('clicked');}button.addEventListener('click', handleClick);button.removeEventListener('click', handleClick);
// Using AbortController for cleanupconst controller = new AbortController();button.addEventListener('click', handleClick, { signal: controller.signal });// Later: remove all listeners attached with this controllercontroller.abort();Event Propagation: Bubbling and Capturing
When an event fires, it travels through the DOM in three phases:
┌─────────────────────────────┐ │ document │ │ ┌───────────────────────┐ │ │ │ body │ │ CAPTURING PHASE │ │ ┌─────────────────┐ │ │ BUBBLING PHASE (top → target) │ │ │ .container │ │ │ (target → top) │ │ │ │ ┌───────────┐ │ │ │ ▲ │ │ │ │ │ button ●──┼──┼──┼───┼─────────┘ │ │ │ │ │ (target) │ │ │ │ ▼ │ │ │ └───────────┘ │ │ │ ───────────────▶ │ │ └─────────────────┘ │ │ TARGET PHASE │ └───────────────────────┘ │ └─────────────────────────────┘
Phase 1: CAPTURING — document → body → container → buttonPhase 2: TARGET — event fires on the buttonPhase 3: BUBBLING — button → container → body → document// Bubbling (default) — handler fires during bubble phasedocument.querySelector('.container').addEventListener('click', (e) => { console.log('Container clicked (bubbling)');});
// Capturing — handler fires during capture phasedocument.querySelector('.container').addEventListener('click', (e) => { console.log('Container clicked (capturing)');}, true); // or { capture: true }
// Stop propagationbutton.addEventListener('click', (e) => { e.stopPropagation(); // prevents further propagation e.stopImmediatePropagation(); // also prevents other handlers on same element});
// Prevent default behaviorconst link = document.querySelector('a');link.addEventListener('click', (e) => { e.preventDefault(); // prevents navigation console.log('Link click intercepted');});Event Delegation
Event delegation leverages bubbling to handle events on many elements with a single listener on a parent:
// BAD: individual listeners on each itemdocument.querySelectorAll('.list-item').forEach(item => { item.addEventListener('click', handleItemClick); // 1000 listeners for 1000 items});
// GOOD: single listener on the parentdocument.querySelector('.list').addEventListener('click', (event) => { // Find the closest .list-item ancestor of the clicked target const item = event.target.closest('.list-item');
// Ignore clicks that are not on a list item if (!item) return;
// Verify the item is inside our list (not a nested list) if (!event.currentTarget.contains(item)) return;
console.log('Clicked item:', item.dataset.id);});Benefits of event delegation:
- Fewer listeners — better memory usage
- Dynamic elements — works for elements added after the listener is set
- Simpler cleanup — remove one listener instead of many
Common DOM Events
| Category | Events |
|---|---|
| Mouse | click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave |
| Keyboard | keydown, keyup, keypress (deprecated) |
| Form | submit, change, input, focus, blur, invalid |
| Document | DOMContentLoaded, load, beforeunload, visibilitychange |
| Scroll | scroll, scrollend |
| Touch | touchstart, touchmove, touchend, touchcancel |
| Pointer | pointerdown, pointermove, pointerup (unifies mouse + touch) |
| Drag | dragstart, drag, dragover, drop, dragend |
| Clipboard | copy, cut, paste |
| Media | play, pause, ended, timeupdate |
| Animation | animationstart, animationend, transitionend |
The Virtual DOM Concept
Frameworks like React introduced the Virtual DOM to optimize DOM updates. Instead of modifying the real DOM directly, changes are made to a lightweight in-memory representation, and only the minimal set of actual DOM operations is applied.
Traditional DOM Manipulation:
State Change → Directly mutate DOM → Browser reflow/repaint(Every change triggers expensive DOM operations)
Virtual DOM Approach:
State Change → New Virtual DOM tree → Diff with previous → Minimal DOM patches │ ┌───────────────┴───────────────┐ │ Reconciliation │ │ (Diffing Algorithm) │ │ │ │ Old VDOM New VDOM │ │ ┌──────┐ ┌──────┐ │ │ │ div │ │ div │ │ │ │ ├─h1 │ │ ├─h1 │ │ │ │ ├─p │ ──▶ │ ├─p* │ ←── changed │ │ └─ul │ │ ├─ul │ │ │ │ ├li│ │ │ ├li│ │ │ │ └li│ │ │ ├li│ │ │ └──────┘ │ │ └li│ ←── added │ │ └────┘ │ │ │ │ Patch: update p text, │ │ append new li │ └───────────────────────────────┘Why Not Just Use the Real DOM?
| Operation | Cost |
|---|---|
| Reading DOM properties | Low |
| Modifying attributes/styles | Medium |
| Adding/removing elements | Medium-High |
| Triggering layout (reflow) | High |
| Painting pixels | High |
| Compositing layers | Medium |
The Virtual DOM batches changes and minimizes the number of expensive real DOM operations. However, it is not always faster than careful manual DOM manipulation — it trades peak performance for developer productivity.
Essential Web APIs
Fetch API
The Fetch API provides a modern interface for making HTTP requests:
// Basic GET requestasync function getUsers() { try { const response = await fetch('https://api.example.com/users');
if (!response.ok) { throw new Error(`HTTP error: ${response.status} ${response.statusText}`); }
const users = await response.json(); return users; } catch (error) { if (error.name === 'AbortError') { console.log('Request was cancelled'); } else { console.error('Fetch failed:', error); } }}
// POST with JSON bodyasync function createUser(userData) { const response = await fetch('https://api.example.com/users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, }, body: JSON.stringify(userData), });
if (!response.ok) { const error = await response.json(); throw new Error(error.message); }
return response.json();}
// With AbortController (timeout/cancellation)async function fetchWithTimeout(url, timeoutMs = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); return response.json(); } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error('Request timed out'); } throw error; }}
// Upload with progress (using XMLHttpRequest — Fetch has no progress events)function uploadWithProgress(file, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload');
xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { onProgress(Math.round((event.loaded / event.total) * 100)); } });
xhr.addEventListener('load', () => resolve(JSON.parse(xhr.response))); xhr.addEventListener('error', () => reject(new Error('Upload failed')));
const formData = new FormData(); formData.append('file', file); xhr.send(formData); });}Web Storage API
// localStorage — persists across sessionslocalStorage.setItem('theme', 'dark');const theme = localStorage.getItem('theme'); // 'dark'localStorage.removeItem('theme');localStorage.clear(); // remove everything
// Store complex data as JSONconst user = { name: 'Alice', preferences: { lang: 'en' } };localStorage.setItem('user', JSON.stringify(user));const saved = JSON.parse(localStorage.getItem('user'));
// sessionStorage — cleared when tab/window closessessionStorage.setItem('tempData', 'value');
// Storage event — fires in OTHER tabs/windowswindow.addEventListener('storage', (event) => { console.log('Key:', event.key); console.log('Old value:', event.oldValue); console.log('New value:', event.newValue); console.log('URL:', event.url);});IntersectionObserver
Efficiently observe when elements enter or leave the viewport:
// Lazy-load imagesconst imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; // load the real image img.classList.add('loaded'); observer.unobserve(img); // stop watching this image } });}, { rootMargin: '200px', // start loading 200px before entering viewport threshold: 0, // trigger as soon as any part is visible});
document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img);});
// Infinite scrollconst sentinel = document.querySelector('#load-more-sentinel');const scrollObserver = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { loadMoreContent(); }}, { threshold: 1.0 }); // fully visible
scrollObserver.observe(sentinel);
// Fade-in animation on scrollconst animObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { entry.target.classList.toggle('visible', entry.isIntersecting); });}, { threshold: 0.1 });
document.querySelectorAll('.animate-on-scroll').forEach(el => { animObserver.observe(el);});Other Important Web APIs
// ResizeObserver — observe element size changesconst resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { console.log('New size:', entry.contentRect.width, entry.contentRect.height); }});resizeObserver.observe(document.querySelector('.responsive-widget'));
// MutationObserver — observe DOM changesconst mutObserver = new MutationObserver(mutations => { mutations.forEach(m => { if (m.type === 'childList') { console.log('Children changed:', m.addedNodes, m.removedNodes); } if (m.type === 'attributes') { console.log('Attribute changed:', m.attributeName); } });});mutObserver.observe(document.querySelector('#dynamic-content'), { childList: true, attributes: true, subtree: true,});
// Clipboard APIasync function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); console.log('Copied!'); } catch (err) { console.error('Failed to copy:', err); }}
// Geolocation APInavigator.geolocation.getCurrentPosition( (position) => { console.log('Lat:', position.coords.latitude); console.log('Lng:', position.coords.longitude); }, (error) => console.error('Geolocation error:', error), { enableHighAccuracy: true, timeout: 5000 });
// History API (SPA routing)history.pushState({ page: 'about' }, '', '/about');history.replaceState({ page: 'home' }, '', '/');window.addEventListener('popstate', (event) => { console.log('Navigation to:', event.state);});Practical Example: Building a Dynamic Todo List
Bringing together DOM manipulation, events, and delegation:
class TodoApp { constructor(containerSelector) { this.container = document.querySelector(containerSelector); this.todos = JSON.parse(localStorage.getItem('todos')) || []; this.render(); this.bindEvents(); }
bindEvents() { // Event delegation on the list this.container.addEventListener('click', (e) => { const deleteBtn = e.target.closest('[data-action="delete"]'); if (deleteBtn) { const id = Number(deleteBtn.closest('li').dataset.id); this.deleteTodo(id); return; }
const checkbox = e.target.closest('input[type="checkbox"]'); if (checkbox) { const id = Number(checkbox.closest('li').dataset.id); this.toggleTodo(id); } });
// Form submission this.container.querySelector('form').addEventListener('submit', (e) => { e.preventDefault(); const input = e.target.querySelector('input[type="text"]'); const text = input.value.trim(); if (text) { this.addTodo(text); input.value = ''; input.focus(); } }); }
addTodo(text) { this.todos.push({ id: Date.now(), text, completed: false, }); this.save(); this.render(); }
toggleTodo(id) { const todo = this.todos.find(t => t.id === id); if (todo) { todo.completed = !todo.completed; this.save(); this.render(); } }
deleteTodo(id) { this.todos = this.todos.filter(t => t.id !== id); this.save(); this.render(); }
save() { localStorage.setItem('todos', JSON.stringify(this.todos)); }
render() { const list = this.container.querySelector('ul') || document.createElement('ul'); list.innerHTML = this.todos.map(todo => ` <li data-id="${todo.id}" class="${todo.completed ? 'completed' : ''}"> <label> <input type="checkbox" ${todo.completed ? 'checked' : ''} /> <span>${this.escapeHtml(todo.text)}</span> </label> <button data-action="delete" aria-label="Delete">×</button> </li> `).join(''); }
escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }}
// Initialize when DOM is readydocument.addEventListener('DOMContentLoaded', () => { new TodoApp('#todo-app');});Performance Tips for DOM Operations
| Tip | Explanation |
|---|---|
| Batch DOM reads and writes | Reading layout properties forces reflow. Read everything first, then write. |
| Use DocumentFragment | Build a subtree in memory, insert once. |
| Debounce scroll/resize handlers | Avoid firing expensive logic on every frame. |
| Use event delegation | One listener on a parent instead of many on children. |
| Prefer textContent over innerHTML | Faster and safer (no HTML parsing). |
| Use requestAnimationFrame | Schedule visual updates for the next repaint. |
| Minimize forced synchronous layouts | Avoid reading layout properties (offsetHeight, getBoundingClientRect) after writes. |
| Detach elements before heavy modification | Remove from DOM, modify, re-insert. |
// Debounce utilityfunction debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); };}
window.addEventListener('resize', debounce(() => { // expensive layout recalculation}, 250));
// requestAnimationFrame for smooth animationsfunction animate() { element.style.transform = `translateX(${position}px)`; position += speed; if (position < maxPosition) { requestAnimationFrame(animate); }}requestAnimationFrame(animate);