How is the jQuery selector $('#foo a') evaluated?
Asked Answered
M

7

44

As a example of jQuery code (https://coderwall.com/p/7uchvg), I read that the expression $('#foo a'); behaves like this:

Find every a in the page and then filter a inside #foo.

And it does not look efficient.

Is that correct? And if yes, how should we do that in a better way?

Minyan answered 3/12, 2012 at 7:10 Comment(2)
@undefined Please look at this: coderwall.com/p/7uchvgMinyan
Thankfully, the post you linked to has been corrected in its comments.Pushed
L
50

That is correct - Sizzle (jQuery's selector engine) behaves the same way as CSS selectors. CSS and Sizzle selectors are evaluated right-to-left, and so #foo a will find all a nodes, then filter those by nodes that descend from #foo.

You improve this by ensuring that your leaf selectors have a high specificity, usually by giving them a class or ID.

Lehr answered 3/12, 2012 at 7:13 Comment(24)
Sizzle might, but... on Chrome, this jsperf shows $('#id span') is 10x faster than $('span'), which I don't think is consistent with the described algorithm.Impecunious
That may be because jQuery has to decorate multiple DOM nodes per run in the span case (more as the test runs), and only one in the #id span case. Check out this jsperf - jsperf.com/does-id-el-find-all-els-first/3 - when you compare LTR to RTL, RTL is faster.Lehr
It is also noteworthy that you can bypass the sizzle engine by using a designated JQuery function such as find() or children(). So instead of $("#foo a") you can use $("#foo").find("a")Demeanor
@Demeanor - Yury's jsperf test case is a brilliant demonstration of that, in fact. It's much faster that way!Lehr
And I've read multiple times that jQuery reads left-to-right where CSS does it right-to-left. Oh the horror of all my inefficient queries! Luckily in the vast majority of cases the speed difference is indistinguishable.Barnyard
Where possible, jQuery passes dom queries to querySelectorAll, which evaluates in whatever order the browser has implemented.Clayborne
@Yury: please elaborate on what you mean by saying Amadan's test case is "wrong". What he says makes perfect sense to me, though I suspect the reason for what he's seeing is a Sizzle optimization ("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.").Pushed
@LarsH: Wrong means it compares apples and oranges. Selectors give different resultset. $("#id span") matches one element while $("span") matches all spans. And that is what actually cause performance difference. AFAIK #id span results in document.querySelectorAll call without any optimizations and such.Clancy
@Yury: Yes they give a different result set. Comparing apples and oranges can be very informative if you interpret the results correctly. It's only incorrect interpretation / application of such a test that is "wrong." I'd be interested to know if your last sentence is correct, since it contradicts the sentence I quoted from the end of the post at net.tutsplus.com/tutorials/javascript-ajax/…Pushed
Oh, come on... Testcase is completely wrong in context of current question. Not sure about optimization. Haven't actually check Sizzle code since jQuery 1.6. But there was no such optimization for 1.6. MB it was added later.Clancy
@Yury: Amadan's test case is relevant when correctly interpreted, as I believe he intended. If Chris is right that "#foo a will find all a nodes, then filter those by nodes that descend from #foo," then it does not make sense that #foo a is faster than a. I think Chris is right in general, but not in this specific case, because of the optimization mentioned. Amadan's test gives us a strong clue that there are important exceptions to the principle Chris described. BTW rather than "apples and oranges" I would call this a comparison of apple flower buds and mature fruit.Pushed
@Pushed Amadan's test is flawed because he never tears down the <span>s added to the document, so many thousands have to be decorated by jQuery. Additionally, it is comparing a single-step query to a multi-step query. See the jsperf test case I linked for a clearer case.Lehr
@LarsH: Sorry, can't agree with you. Jsperf tests are created to compare performance of different code samples addressing the SAME problem. Can I replace snippet A with snippet B? No. Why on Earth would I compare them? They are not interchangeable. Period.Clancy
@Yury: You're unnecessarily limiting the usefulness of jsperf. If Bob takes 2 hours to get from A to B by car, and Jack takes 10 minutes to get from A to C on bicycle, you can be pretty sure A to C is a shorter distance, even tho a bicycle is not a car. Apples to apples is just one kind of useful test.Pushed
Chris, I'm not entirely sure what is meant by "tears down" or "decorated", but I think I see your point: an alternative explanation of Amadan's results (which validates his test, properly interpreted). Regarding a comparison of single-step to multistep, see my comments to Yury. I agree that the test case you linked to is clearer (more comprehensive). I was arguing that Amadan's test is also valuable, and I have yet to see a flaw in it. The only flaw I see is in others assuming (without support) that Amadan was suggesting an incorrect interpretation of his results.Pushed
He removes the divs in the teardown step, but not the spans. Thus, on every test run (of which there are a lot) more spans get appended to the DOM. This isn't testing the speed of the selector; it's demonstrating that jQuery takes longer to decorate more objects after it's found them, which is known and not contested. It obscures the contended point of the benchmark by hiding it behind another performance bottleneck.Lehr
Chris, I see what you're saying about some of the <span> elements not being removed in the teardown. (In that regard, Yury's test case is unfortunately just as "wrong" as Amadan's.) I see that a new revisions of the jsperf test case are being created, correcting this problem, so I'll go take a look.Pushed
I added another revision (jsperf.com/does-id-el-find-all-els-first/8), to be more representative of cases where performance would matter, i.e. where there are more elements of the selected type, especially outside of the #id element. Interesting that Firefox and IE are faster on 'el unrestricted' until you add more spans - supporting your hypothesis about decoration time being the dominant factor.Pushed
That's a bad benchmark, though, because you're introducing additional variables. A good benchmark eliminates all variables except the one you're trying to measure. You should do two benchmarks, one for selector speed, and one for decoration speed, if you want useful numbers.Lehr
Do you mean rev 8 is a bad benchmark? I haven't introduced any variables, only changed the value of a variable that was already there (the number of span elements) to better reflect the environment that the OP has in mind: one where the number of elements of the given type (a or span) is significant. I would call a more representative benchmark a better benchmark.Pushed
Yes; you aren't measuring one thing, you're measuring two at the same time. I don't mean a programming variable here - I mean that you have two variables (time to select and time to wrap) which are being measured in a single benchmark, and you can't tell how much of the operation belongs to each. If you want to know how fast selectors are, measure just selectors. If you want to know how fast wrapping is, measure just wrapping. Measuring both at once invalidates the usefulness of the benchmark.Lehr
let us continue this discussion in chatPushed
My test using jsperf.com/does-id-el-find-all-els-first/4 shows $("#id").find("span") faster than $('#id span'), could anybody explain why?Clerical
@Clerical That's because of this same issue; $("#id").find("span") finds all the #id elements (of which there is 1) and wraps them, then finds all the span elements within it. $("#id span") finds all spans in the document, then determines which of those descend from #id and wraps them.Lehr
D
19

how should we do that in a better way?

Use the context parameter from jQuery.

$('a', '#foo');

Now jQuery will search all anchors within the context of the element with id: foo.

In your query the context is defaulted to document when omitted:

$('#foo a'); == $('#foo a', document); 

In this case, your query is indeed not efficient.

You might take a look at this article.

Dartmouth answered 3/12, 2012 at 7:14 Comment(2)
you can also measure it on jsperf.com jsperf.com/popular --> some examplesRomaine
You should always pass a DOM element as the context argument, not a selector. jsperf.com/jquery-context-testSacrilege
B
5

While it is true that Sizzle is a right-to-left engine (which is the same way css is interpreted), it is not true that the specific selector in your example would select all anchor elements on the page and then filter their parents to match the id of "foo". Sizzle actually optimizes any selector that starts with an ID and uses that as the context for the entire selection, rather than using the document. In other words, the selector you've chosen basically translates to:

document.getElementById("foo").getElementsByTagName("a")

Really, that's not a bad selector at all.

However, given the other things jQuery needs to do (which includes looping over the elements to merge them onto the jQuery instance), jQuery("#foo").find("a") will always be the fastest because jQuery implements a jQuery object creation shortcut for id-only selectors, and then it does the find rooted from #foo.

In other words, Sizzle itself is not much different when doing Sizzle("#foo a") and Sizzle("a", document.getElementById("foo")), but jQuery("#foo").find... will be faster because of jQuery's own ID shortcut.

By the way, my remarks on Sizzle is assuming querySelectorAll is not being used. If it is, Sizzle just passes it on to qsa, which still isn't as fast as using jQuery's ID shortcut.

Buote answered 6/2, 2013 at 17:3 Comment(0)
T
4

You can use find() for more granular control on your selector order:

$('#foo').find('a');

This will of course be more impressive with more complex selectors, where you can chain find() and filter().

For the record $('#foo').find('a') === $('a','#foo')

[Update] ok, I realized later that it's exactly what your link says...

The jQuery selector engine (Sizzle) has been refactored last year, you'll find detailed explanations here: http://www.wordsbyf.at/2011/11/23/selectors-selectoring/

Thickskinned answered 3/12, 2012 at 7:33 Comment(0)
R
2

Instead of filtering with a inside #foo elements, simply attach a class to a elements and get a elements with class like $("a.class");. This would be more efficient.

Rile answered 3/12, 2012 at 7:13 Comment(0)
D
0

Yet another "try it for yourself":

  1. jsperf for various selectors on 10000 elements
  2. jsperf for various selectors on 300 elements
  3. jsperf for various selectors on a "more representative DOM"

Doesn't seem to be much difference with a "flat" DOM (1 & 2), but the performance varies much more with a nested DOM.

Also note that some of the test cases aren't selecting the correct elements (i.e. $('.a') vs $('.a', context)), but I left them from the original tests just for comparison.

Damascene answered 29/3, 2013 at 14:57 Comment(0)
B
-4

This example will retrieve the all anchors elements a in an element called foo, to Find every a in the page and then filter a inside #foo as you want u should select a #foo

$("a #foo");

this will retrieve all the foo elements inside a elements.

Boehmenism answered 3/12, 2012 at 7:21 Comment(2)
"a #foo" is pretty much suboptimal. You can have only 1 element with id="foo".Clancy
I don't see why this answer got so many downvotes. I think Mr.H misunderstood the question, but the question was poorly worded. "Filter a inside #foo" is hardly standard grammar.Pushed

© 2022 - 2024 — McMap. All rights reserved.