As English speakers, our minds are geared toward interpreting data and text from left-to-right. However, as it turns out, many of the modern JavaScript selector engines (jQuery, YUI 3, NWMatcher), and the native querySelectorAll
, parse selector strings from right to left.
It's important to note that, more often than not, you don't need to worry about selector performance too much -- as long as your selectors aren't obnoxious. jQuery's Sizzle is incredibly fast and accommodating.
An Example
Consider the following selector:
$('.box p');
Though some -- generally older -- selector engines will first query the DOM for the element with a class
of box
, and then move on to finding any p
tags which are children, jQuery works in reverse. It begins by querying the DOM for all paragraph tags on the page, and then works it way up the parent nodes and searches for .box
.
JSPerf
We can use the excellent JsPerf.com website to test this out.
// The markup <div id="box"> <p> Hello </p> </div> // The Test //1 . $('#box p'); // 2. $('#box').find('p');
The image above shows that using find()
or children()
is roughly 20-30% faster, depending upon the browser.
The jQuery library has an optimization that will immediately determine if an id
was passed to the jQuery object ( $('#box')
). If that's the case, it doesn't need to use Sizzle; instead, it quickly passes the selector to getElementById
. And, of course, if the browser is modern enough, querySelectorAll
will take over for Sizzle.
On the other hand, with $('#box p')
, jQuery needs to parse this string with the Sizzle API, which will take a bit longer (though Sizzle does have an optimization for selectors that begin with an id
). This is precisely why it's also marginally faster to do things like $('.elems').first()
over $('.elems:first')
. The latter selector will need to be parsed.
Another Example
Let's review another example:
$('#container > :disabled');
This selector seems appropriate. Find all disabled inputs (or actually, elements) that are within #container
. However, as we've learned, jQuery and the native querySelectorAll
work right-to-left. This means that jQuery will grab, literally, every element in the DOM, and determine if its disabled
attribute is set to true. Notice that there's no pre-filtering to first find all inputs on the page. Instead, every element in the DOM will be queried.
// From the jQuery Source disabled: function( elem ) { return elem.disabled === true; }
Once it's compiled a collection, it then travels up the chain to the parent, and determines if it's #container
. Certainly, this isn't effective, and, though it's true that perhaps too much attention in the community is paid to selector performance, we should still strive to not write overly intensive selectors, when possible.
You can improve this selector a bit by doing:
// Better $('#container > input:disabled');
This code will limit the query to all inputs on the page first (rather than every element). Even better, though, we can again use the find
or children
method.
$('#container').children('input:disabled');
Don't Worry Too Much
It's important for me to reiterate that you honestly don't need to worry about selector performance too much. There are plenty of optimizations in jQuery that will assist you. It's generally better to focus on bigger ticket items, like code organization and structure.
As an example, if Sizzle comes across a selector like $('#box p')
, it's true that it works right-to-left, but there's also a quick regex optimization that will first determine whether the first section of the selector is an id
. If so, it'll use that as the context, when searching for the paragraph tags.
Nonetheless, it's always helpful to know what's happening behind the scenes -- at least at a very low level.
Comments