~ 2 min read

Why element.querySelector() Isn't Just Scoped Search


I used to think someElement.querySelector('.item') would be faster than document.querySelector('.item') since it only searches a smaller part of the DOM. But while trying to optimize my querySelector calls in an Astro component, I realized something didn’t add up.

The Question That Changed My Mind

If element.querySelector() only searched within that element’s subtree (the “obvious logic”), how would these selectors behave?

// With "normal logic" (hypothetically):
container.querySelector('body .item'); // ❌ Would break - 'body' isn't under container
container.querySelector(':not(.hidden) .item'); // ❌ How would this even work?
container.querySelector('.ancestor .item'); // ❌ What if ancestor isn't under container?

Yet, these selectors work just fine. So the browser isn’t doing what I expected.

Here’s What Actually Happens

CSS selectors have global semantics. When you write .item, it means “any element with class item” — not “relative to where I’m calling from.”

So when you call element.querySelector(), the browser actually:

  1. Evaluates the selector globally - as if searching the entire document
  2. Filters the results - keeps only matches that are descendants of your element

This is actually mentioned in the MDN documentation

The Implication

This means element.querySelector() is actually doing more work than document.querySelector() — not less — because of the extra filtering step. My intuition about performance was completely backwards.

Lesson learned: Don’t assume how APIs work under the hood. Always question the edge cases.


Headshot of Nam Le

Hi, I'm Nam Le. I'm a software engineer based in HCM city, Vietnam. You can follow me on Twitter, see some of my work on GitHub, or connect with me on LinkedIn.