The other day, on my blog post about creating custom jQuery selectors, Scott Smith mentioned to me that he was having some performance issues with a custom selector. Specifically, he mentioned that in some experimentation, he noticed that his custom selector was being run against more nodes than he expected. Until then, I had never really thought deeply about how a custom jQuery selector was actually executed, so I figured I'd do some experimentation of my own.
In the following code, I have a Span element wrapped in a series of nested Div elements. Two of the outer-most Div elements have the class, "parent":
<div class="parent"> <div class="parent"> <div> <span>Here is some inner text.</span> </div> </div> </div>
When I run the code (below), I'm going to locate the Span element; then, I'm going to call the parents() method on it, using the following selector:
Notice that we are running 4 tests on each node returned by parents():
- Test for the Div tag.
- Test for the class, ".foo".
- Test for the custom jQuery selector, ":testSelector".
- Test for the class, ".bar".
The custom jQuery selector, ":testSelector," simply logs its invocation arguments to the console such that we can see where in the process it is being called.
Now that you have an idea of what I'm planning to do, let's take a look at the actual code:
As you can see, the ":testSelector" custom jQuery selector simply outputs the node being examined, its index, and the collection from which it came. When we run this code, here is what FireBug is telling me:
This is very interesting! Look at the nodes in the collection being examined by the custom selector:
This completely changes the way I thought selectors were executed. I understand that Sizzle runs tree traversal in reverse; however, as my tests are all on the same node, I figured that would not matter. Of course, this has nothing to do with forward vs. reverse selecting - this has to do with progressive collection trimming. It appears that each selector execution does not perform any progressive collection trimming; meaning, neither the "div" selector, nor the ".foo" selector, nor the ".bar" selector had any pre-trimming affect on the node collection before it was passed into the testSelector custom jQuery selector method.
This is fascinating! As far as ".foo", ".bar", and ":testSelector" go, I figured I might not be able to count on order; but, I was almost certain that the "Div" tag selector would have done at least some pre-trimming of the collection. After all, I thought Sizzle used the getElementsByTagName() method.
Of course, it's not quite that straightforward! This exploration used the parents() method; if I were to replace the above use of parents() with any of the following direct jQuery selections:
- $( "div.foo:testSelector.bar" );
- $( "div.foo:testSelector" );
- $( ".foo:testSelector" );
... then, the ":testSelector" method would never get executed. As it turns out, when you do direct selections, jQuery does perform progressive trimming of the target node collection. I assume it does this using variations of getElementsByTagName(), getElementsByClassName(), and whatever new query methods browsers now support, before the node collection is passed off to (or rather, not off to) ":testSelector."
Ultimately, it doesn't really matter how the node collections get trimmed, since undesired nodes will be removed one way or another based on your selector; but, it's something that I have never thought deeply about before, and it's interesting to see how selector behavior changes (behind the scenes) based on the context of the selection.
Want to use code from this post? Check out the license.