Explain the difference between `document.querySelector()` and `document.getElementById()`
TL;DR
document.querySelector() and document.getElementById() are both methods used to select elements from the DOM, but they have key differences. document.querySelector() can select any element using a CSS selector and returns the first match, while document.getElementById() selects an element by its ID and returns the element with that specific ID.
// Using document.querySelector()const element = document.querySelector('.my-class');// Using document.getElementById()const elementById = document.getElementById('my-id');
Difference between document.querySelector() and document.getElementById()
document.querySelector()
- Can select elements using any valid CSS selector, including class, ID, tag, attribute, and pseudo-classes
- Returns the first element that matches the specified selector
- More versatile but slightly slower due to the flexibility of CSS selectors
// Select the first element with the class 'my-class'const element = document.querySelector('.my-class');// Select the first <div> elementconst divElement = document.querySelector('div');// Select the first element with the attribute data-role='button'const buttonElement = document.querySelector('[data-role="button"]');
document.getElementById()
- Selects an element by its ID attribute
- Returns the element with the specified ID
- Faster and more efficient for selecting elements by ID, but less versatile
// Select the element with the ID 'my-id'const elementById = document.getElementById('my-id');
Key differences
- Selector type:
document.querySelector()uses CSS selectors, whiledocument.getElementById()uses only the ID attribute. - Return value:
document.querySelector()returns the first matching element, whereasdocument.getElementById()returns the element with the specified ID. - Performance:
document.getElementById()is generally faster because it directly accesses the element by ID, whiledocument.querySelector()has to parse the CSS selector.
The full DOM-query method comparison
querySelector and getElementById are two of the seven main DOM-query methods. The most important practical distinction across all of them is whether the result is a live or a static collection.
| Method | Selector input | Returns | Live? | When to use |
|---|---|---|---|---|
getElementById(id) | id string | Element or null | No (single element) | Single element by id; hot-path code |
querySelector(sel) | Any CSS selector | First match or null | No (snapshot) | First element matching any selector |
querySelectorAll(sel) | Any CSS selector | Static NodeList | No (snapshot) | All matches as a frozen list |
getElementsByClassName(name) | Class name | HTMLCollection | Yes (auto-updates) | When you need a live collection |
getElementsByTagName(tag) | Tag name | HTMLCollection | Yes | Same |
getElementsByName(name) | name attribute | NodeList | Yes | Form elements by name |
closest(sel) | Any CSS selector | Nearest matching ancestor (or self) or null | N/A | Walking up from a target inside event handlers |
The live vs snapshot distinction is a common source of subtle bugs.
Live vs static collections (predict the output)
document.body.innerHTML = '<div class="x"></div><div class="x"></div>';const live = document.getElementsByClassName('x'); // HTMLCollection (live)const snapshot = document.querySelectorAll('.x'); // NodeList (static)console.log('before:', live.length, snapshot.length); // 2, 2document.body.insertAdjacentHTML('beforeend', '<div class="x"></div>');console.log('after: ', live.length, snapshot.length); // 3, 2
The live HTMLCollection updates automatically when DOM nodes are added or removed. The static NodeList from querySelectorAll does not.
This matters in practice in two specific ways:
- Iterating a live collection while mutating it is a classic infinite-loop bug. Reading
live[i]after appending more matching nodes hits the new ones too, and theforloop never finishes. - A cached
querySelectorAllresult is a stable snapshot; a cachedgetElementsByClassNameresult changes as the DOM changes. Neither is universally better: the snapshot can become stale, while the live collection can change during iteration.
Prefer querySelectorAll for most modern use cases (it has forEach, and array spread [...] works on it). Reach for the live collections only when you specifically want auto-updating behavior.
Performance: how much does it actually matter?
getElementById() gives the browser a narrower lookup than a general CSS selector, but engine strategies and timings vary with the document and browser. In ordinary application code, choose the API that expresses the lookup clearly. If a real performance trace identifies repeated DOM queries in a hot path, reduce the number of queries or cache a valid reference and measure again.
Complex selectors may require more work than an ID lookup, but invented per-call timings and isolated microbenchmarks do not establish user impact. DOM mutation, style, layout, rendering, and the work performed after selection are often more significant.
If you genuinely need maximum speed for repeated lookups, cache the reference:
const button = document.getElementById('action');button.addEventListener('click', handle);
Keep in mind that a cached element can become detached or be replaced. A long-lived reference is correct only while that element remains the intended target.
What about closest(), matches(), and contains()?
Three more methods round out the modern DOM-query toolkit:
-
element.closest(selector)walks up from the element (including the element itself) and returns the nearest ancestor matching the selector, ornull. It is constantly useful in event-delegation handlers:table.addEventListener('click', (event) => {const row = event.target.closest('tr[data-id]');if (row) editRow(row.dataset.id);}); -
element.matches(selector)returnstrueorfalsefor whether the element matches the CSS selector. Useful inside delegated handlers when you want to confirm the target type without atagNamecheck. -
parent.contains(child)returnstrueifchildisparentor anywhere inside it. Useful for outside-click detection:if (!modal.contains(event.target)) close().
These three plus querySelector and querySelectorAll form the modern toolkit. The older getElementsBy* methods are legacy and rarely the right default in new code.