Chapter 8.2☕ 18 min read

DOM Selection: Every Method Compared & Performance

getElementById O(1) hai, querySelector slow hai, Live collection dangerous hai. Har method ka andar — yeh chapter mein.

01Classic Selectors — getElementById, getElementsByTagName

getElementById uses an internal ID hash map maintained by the DOM engine — O(1) average lookup. The browser doesn't walk the tree; it goes directly to the element.

Returns a single Element or null. IDs MUST be unique — if duplicates exist, the first match in document order wins.

// Fastest DOM selection method
const header = document.getElementById('main-header');
console.log(header); // Element or null

// ID lookup is O(1) — browser uses internal hash map
// Even on a page with 100,000 elements, it's instant!

// ❌ Don't do this — unnecessarily slow for ID lookup
const header2 = document.querySelector('#main-header');
// This parses the CSS selector, compiles it, THEN looks up
// getElementById skips all of that!

// ID must be unique — if not, first match wins
// <div id="item">First</div>
// <div id="item">Second</div>
document.getElementById('item'); // Always returns FIRST div
How the hash map works: getElementById uses a hash table maintained by the DOM engine. The hash table is populated during HTML parsing — each element with an id attribute gets an entry. Lookup is O(1) because it's a direct hash map probe, not a tree walk.

When to use: Any time you need a single element by ID and performance matters. This is always the fastest selection method.

02Modern Selectors — querySelector aur querySelectorAll

getElementsByClassName returns a LIVE HTMLCollection — it auto-updates when the DOM changes. Add a new matching element, and the collection grows instantly.

getElementsByTagName also returns a LIVE HTMLCollection — same behavior, matches by tag name instead.

getElementsByTagName('*') returns ALL elements on the page — useful for counting or debugging.

HTMLCollection does NOT have .forEach() — you must convert first: [...collection] or Array.from(collection).

Multiple classes: getElementsByClassName('a b') finds elements with BOTH classes a AND b (AND logic).

// Live collection — changes when DOM changes
const items = document.getElementsByClassName('item');
console.log(items.length); // 5

// Add a new element with class "item"
const newItem = document.createElement('div');
newItem.className = 'item';
document.body.appendChild(newItem);

console.log(items.length); // 6! Auto-updated!

// ❌ No forEach on HTMLCollection
// items.forEach(item => {}); // TypeError!

// ✅ Convert to array first
[...items].forEach(item => console.log(item));
Array.from(items).forEach(item => console.log(item));

// Multiple classes — AND logic
const abItems = document.getElementsByClassName('class-a class-b');
// Matches elements that have BOTH class-a AND class-b

// All elements on page
const all = document.getElementsByTagName('*');
console.log(all.length); // Total elements in DOM
Live vs Static: Live collections are dangerous when iterating + modifying. If you remove an element during a forward loop, indices shift and you skip elements. Always convert to array before modifying: [...items].forEach(...) takes a snapshot, so removals are safe.
03Traversal — parentNode, children, nextSibling & Closest

querySelector returns the FIRST matching Element or null. Uses full CSS selector syntax — powerful but slower than getElementById.

querySelectorAll returns a STATIC NodeList — a snapshot that doesn't update when DOM changes. Safe to iterate and modify.

CSS selector power: '.parent > .child:nth-child(2)', '[data-id="123"]', 'div:not(.excluded)'.

NodeList from querySelectorAll HAS .forEach() — but it's still not a real Array (no .map, .filter, .reduce).

querySelector is SLOWER than getElementById for ID lookups — must parse + compile the CSS selector first.

// querySelector — first match only
const firstItem = document.querySelector('.item'); // First .item or null

// querySelectorAll — all matches, STATIC NodeList
const allItems = document.querySelectorAll('.item'); // NodeList
allItems.forEach(item => console.log(item)); // ✅ forEach works!

// Powerful CSS selectors
const secondChild = document.querySelector('.list > li:nth-child(2)');
const byDataAttr = document.querySelector('[data-user-id="123"]');
const notExcluded = document.querySelectorAll('div:not(.exclude)');
const deepNested = document.querySelector('.parent .child.grandchild');

// Static = safe to modify during iteration
allItems.forEach(item => {
  item.remove(); // Safe! Collection doesn't change.
});

// Convert NodeList to real Array for full array methods
const itemsArray = [...allItems]; // Now has .map, .filter, .reduce
const names = itemsArray.map(el => el.textContent);
📋 When to use what: Use querySelector/querySelectorAll when you need CSS selector power (combinators, pseudo-classes, attribute selectors). Use getElementById when you just need an ID lookup — it's faster.
04Live vs Static Collections — HTMLCollection vs NodeList

closest(selector): Walks UP from the element, returns the first ancestor (or self) matching the selector. Essential for event delegation.

matches(selector): Checks if the element matches a selector — boolean check, no traversal needed.

contains(otherNode): Checks if otherNode is a descendant of (or equal to) the element.

closest() includes the element itself — if the element matches, it returns itself immediately.

closest() returns null if no match found — always null-check the result!

// closest() — walk UP the tree
const button = document.querySelector('button');
// <div class="card"> → <div class="list"> → <button>
const card = button.closest('.card'); // Finds .card ancestor
const list = button.closest('.list'); // Finds .list ancestor
const body = button.closest('body');  // Goes all the way up

// closest() includes self
const self = button.closest('button'); // Returns button itself!

// matches() — just check, don't traverse
button.matches('.active'); // true or false
// Useful in event delegation:
document.addEventListener('click', (e) => {
  if (e.target.matches('.delete-btn')) {
    // Handle delete
  }
});

// contains() — descendant check
document.body.contains(button); // true — button is in body
button.contains(button);       // true — contains itself!
card.contains(button);         // true — button is inside card
button.contains(card);         // false — card is not inside button
closest() performance: It internally walks up using parentElement — worst case O(tree depth) per call. But tree depth is typically less than 20, so it's negligible. The browser's CSS engine compiles the selector once and reuses it for matching at each level.
05Performance Showdown — Kaun Kitna Fast Hai?

Not every navigation need is the same. Here's every property compared — the "Element" variants skip text nodes, the "Node" variants include them.

// Navigation cheat sheet
// ────────────────────────────────────────
// PARENT:
element.parentElement;  // Parent Element (never document)
element.parentNode;     // Parent Node (could be document)

// CHILDREN:
element.children;             // Element children only (live HTMLCollection)
element.childNodes;           // All children including text (live NodeList)
element.firstElementChild;    // First child ELEMENT
element.firstChild;           // First child NODE (might be text!)
element.lastElementChild;     // Last child ELEMENT
element.lastChild;            // Last child NODE (might be text!)
element.childElementCount;    // Number of element children

// SIBLINGS:
element.nextElementSibling;     // Next sibling ELEMENT
element.nextSibling;            // Next sibling NODE (might be text!)
element.previousElementSibling; // Previous sibling ELEMENT
element.previousSibling;        // Previous sibling NODE (might be text!)

childNodes includes whitespace text nodes between elements — this trips up beginners constantly.

// <div id="parent">
//   Text node (whitespace)
//   <span>A</span>
//   Text node (whitespace)
//   <span>B</span>
//   Text node (whitespace)
// </div>

const parent = document.getElementById('parent');

// ❌ Dangerous — includes text nodes
parent.childNodes;      // NodeList: [text, span, text, span, text]
parent.firstChild;      // Text node (whitespace!), NOT <span>

// ✅ Safe — elements only
parent.children;        // HTMLCollection: [span, span]
parent.firstElementChild; // <span>A</span>
parent.lastElementChild;  // <span>B</span>
parent.childElementCount; // 2
📋 Golden rule: ALWAYS prefer the "Element" versions (children, firstElementChild, nextElementSibling) unless you specifically need text nodes. The Node versions will bite you with unexpected whitespace text nodes.

Lo kar liya — Key Points:

  • ✅ getElementById uses an internal O(1) hash map — fastest selection method
  • ✅ getElementsByClassName/getElementsByTagName return LIVE HTMLCollections that auto-update when DOM changes
  • ✅ querySelector/querySelectorAll use CSS selector syntax and return static results — safe to iterate and modify
  • ✅ HTMLCollection does NOT have .forEach() — convert to array with [...collection] or Array.from()
  • ✅ closest(selector) walks UP the tree to find matching ancestor — essential for event delegation
  • ✅ matches(selector) tests if an element matches a CSS selector — boolean check without traversal
  • ✅ Always prefer Element navigation (children, firstElementChild, nextElementSibling) over Node navigation to avoid text nodes
  • ✅ childNodes includes whitespace text nodes; children returns only Element nodes
Course Search
Search across all chapters & stages
📖

Search the course

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

merge branchesgit stashundo commitrebase