I've come into the habit of altering my jQuery queries to hopefully optimise them to run faster. For example, if I wanted to get the html of the first "a" element...
$("a:first").html()
// instead of this, which is functionally identical
$("a").html()
It makes sense right? Don't bother getting all the elements, just stop at the first one. Today I got to thinking about this, and thought I should benchmark it to see how much faster it actually is. I'll spare you the full code I ran, but here's the gist. Put 1000 links on a page, and then run a series of selectors on that page 1000 times each.
$("a"); // 1133ms
// now the queries to get just the first one
$("a:first"); // 6198ms
$("a:eq(0)"); // 6922ms
$(":first"); // 6115ms
$("a").eq(0); // 1305ms
// (jQuery 1.3.2)
Yikes! Using the condition in the query itself is FIVE times slower!!
I followed the execution path and found out that jQuery does not optimise for the fact that you're only trying to select one element, and it loops through the entire set to find the first one! Here's the pseudo-code
- First, break the query into its parts: "a:first" -> "a", ":first"
- Then evaluate each part:
- "a": Since it's a tag name, finding all the "a" elements is really fast.
- ":first": This is a positional selector.
- Loop through the current set (1000 items)
- Look at our selector and compare its rules to the index of the current element.
ie: Is the current index == 0? If yes, ok let it stay in the set, otherwise remove it. - Repeat through all elements, despite already filling all the criteria
This is the same execution plan for all the positional pseudo selectors (:first, :last, :even, :odd, :eq(), :gt() and :lt()), which is absolutely crazy. For :even and :odd it's always to be an O(n) function, looping through them all removing every second one, but for the rest, it should be O(1)! There only are half a dozen selectors, all you'd need to do is add a tiny bit of logic to resolve :first to .slice(0, 1), :last to .slice(-1), :lt(3) to .slice(0, 3), etc.
Anyway, I'm not going to go about hacking the core code right now. All I can really do is learn from this and recommend that you too avoid the positional pseudo selectors whenever possible. Instead, use .eq() and .slice().
$("div:lt(3) p:gt(1) span:first a:last") // slow
$("div") // awfully wordy, but quick
.slice(0, 3)
.find("p")
.slice(1)
.find("span")
.eq(0)
.find("a")
.slice(-1)
;
Update!
Perhaps I was jumping the gun before. As I noted, one operation is virtually O(1) whereas the other is O(n). Obviously the O(1) function is going to be better, but at low values of n it's really not going to matter that much, is it? I figured I should investigate a few more variables - notably, what happens if you vary the number of elements you want the "first" one of? In my previous test, I'd only tested against a first-pass match of 1000 elements, but realistically, how often do you write a jQuery query which finds 1000 elements?
Anyway, I've put up an interactive test so you can try it out for yourself. You select the maximum number of elements to include, and it tests varying amounts up to that number against both methods - and it even graphs it for you! Go check it out!