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?

Keavon
  • 6,837
  • 9
  • 51
  • 79
Afshin Mehrabani
  • 33,262
  • 29
  • 136
  • 201

7 Answers7

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.

Chris Heald
  • 61,439
  • 10
  • 123
  • 137
  • 8
    Sizzle might, but... on Chrome, [this jsperf](http://jsperf.com/does-id-el-find-all-els-first) shows `$('#id span')` is 10x faster than `$('span')`, which I don't think is consistent with the described algorithm. – Amadan Dec 03 '12 at 07:19
  • 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 - http://jsperf.com/does-id-el-find-all-els-first/3 - when you compare LTR to RTL, RTL is faster. – Chris Heald Dec 03 '12 at 07:33
  • 8
    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")` – Matanya Dec 03 '12 at 07:34
  • @Matanya - Yury's jsperf test case is a brilliant demonstration of that, in fact. It's much faster that way! – Chris Heald Dec 03 '12 at 07:35
  • 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. – Fergal Dec 03 '12 at 12:13
  • 1
    @YuryTarabanko: Thank you, that was enlightening. – Amadan Dec 03 '12 at 14:14
  • Where possible, jQuery passes dom queries to `querySelectorAll`, which evaluates in whatever order the browser has implemented. – zzzzBov Dec 03 '12 at 15:08
  • @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."). – LarsH Dec 03 '12 at 16:03
  • @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. – Yury Tarabanko Dec 03 '12 at 16:42
  • @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 http://net.tutsplus.com/tutorials/javascript-ajax/quick-tip-think-right-to-left-with-jquery/ – LarsH Dec 03 '12 at 16:48
  • 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. – Yury Tarabanko Dec 03 '12 at 16:59
  • @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. – LarsH Dec 03 '12 at 17:08
  • @LarsH Amadan's test is flawed because he never tears down the 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. – Chris Heald Dec 03 '12 at 19:12
  • @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. – Yury Tarabanko Dec 03 '12 at 19:20
  • @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. – LarsH Dec 03 '12 at 19:26
  • 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. – LarsH Dec 03 '12 at 19:35
  • 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. – Chris Heald Dec 03 '12 at 19:48
  • Chris, I see what you're saying about some of the `` 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. – LarsH Dec 03 '12 at 20:30
  • I added another revision (http://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. – LarsH Dec 03 '12 at 21:18
  • 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. – Chris Heald Dec 03 '12 at 21:51
  • 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. – LarsH Dec 03 '12 at 22:31
  • 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. – Chris Heald Dec 03 '12 at 22:42
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/20539/discussion-between-larsh-and-chris-heald) – LarsH Dec 04 '12 at 01:00
  • My test using http://jsperf.com/does-id-el-find-all-els-first/4 shows ``$("#id").find("span")`` faster than ``$('#id span')``, could anybody explain why? – coderz Jan 19 '15 at 16:09
  • @coderz 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. – Chris Heald Jan 19 '15 at 18:00
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.

iHiD
  • 2,450
  • 20
  • 32
Andries
  • 1,547
  • 10
  • 29
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.

timmywil
  • 311
  • 2
  • 6
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/

Christophe
  • 27,383
  • 28
  • 97
  • 140
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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Murali N
  • 3,451
  • 4
  • 27
  • 38
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.

drzaus
  • 24,171
  • 16
  • 142
  • 201
-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.

Mr.H
  • 61
  • 8
  • 6
    "a #foo" is pretty much suboptimal. You can have only 1 element with `id="foo"`. – Yury Tarabanko Dec 03 '12 at 07:23
  • 5
    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. – LarsH Dec 03 '12 at 19:14