Ever since we began the Dojo project, it's been our intention to collect the very best, most useful JavaScript and consolidate it into a single library that makes your applications better and easier to use. I'm excited to announce a new evolution in Dojo that continues this tradition: dojo.query
Over the last year jQuery, MochiKit, Prototype, and behavior.js have been emphasizing the importance of being able to easily query arbitrary HTML DOM structures via CSS-like syntax. The importance of these APIs hasn't been lost on us at Dojo, but the lack of efficiency in most of the available libraries gave us pause. We need a generalized query system that is both powerful yet fast enough to not endanger the user experience when used heavily.
It's an important goal, and ensuring that simple-looking queries don't "hurt" disproportionately is a difficult task. After some initial work with my admittedly grotty getElementsById hack, my outlook wasn't bright. Other systems weren't looking much better. Using the behavior: expression(); hack degrades page performance something fierce and can't be made synchronous, a key requirement to meet developer ease-of-use goals. Requiring a callback for every query result is a no-go.
Luckily Jack Slockum's recent work has pointed the way to a query system that is fast enough for heavy use without waiting on the browser vendors to get their act together and give us a compact, powerful, and fast selector system. As a piece of engineering work, DomQuery is a beauty. It's fast, small, and has few dependencies. Without Jack's excellent example, I might have left the idea of a query system for Dojo on the shelf even longer. In particular, DomQuery starts to address some of the largest issues I had identified when investigating getElementsById:
- A generic system must have enough fast-path differentiation to keep queries that are known by webdevs to be fast running at something like "native" speed.
- It is not clear that the IE team has any plans to embrace XPath over the HTML DOM despite its clear utility and proven value. Assuming that there will continue to be bifurcation in browser-provided fast-paths for query syntaxes, it's reasonable to assume that CSS style selectors will most likely be made available natively everywhere. So what's the right query language subset for a JS library to support?
- Current systems get choked up on queries that potentially exhibit large "node blooms" in the intermediate phases of constructing the result set. Queries of the form "div div div" give most systems a hell of a time, reducing user trust in the system. Can we level out that anomaly?
The last part is critically important because it highlights how innocent looking queries can endanger the developer and user experience. Simple interfaces that allow you to hurt yourself easily may be worse than more complicated paths into the same functionality. API ease should scale with utility, but also with expected pain or speed tradeoffs. A dangerous API should look dangerous. The gauntlet is thus laid: can we go fast enough on hard queries to provide a trustable foundation for large-scale development?
Yes. We can.
With the recent DomQuery/jQuery debate as a backdrop, I started to investigate the relative performance of some of the more esoteric options. From caching behavior hacks on IE to XPath to getElementsById, everything was on the table. The results surprised me.
In many cases, the availability of XPath isn't necessarily a win until the complexity (node-bloom size) of a query outweighs what appears to be non-trivial startup costs for the query engine. On Firefox in particular, XPath queries can yield tremendous gains for generalized queries over all nodes with a particular attribute value or over queries that look like "div div div". Surprisingly, though, simpler queries such as "div div" may actually be at a loss to regular getElementsByTagName() attacks. Confoundingly, the results are also browser specific. Not all queries that run faster in XPath vs. DOM on Firefox run faster via XPath on Opera or the WebKit nightly builds. Nevertheless, for many of the hardest queries, deferring to XPath can yield results that are 2x-4x better than other approaches.
Of course, the 80% browser doesn't support XPath, but I'm personally over losing sleep to Microsoft's bad engineering and management decisions. If the IE team decides to get with the program and give us a good query engine, we'll use it. Until then, they get best effort. Their browser only deserves to look as good (or bad) as it really is, after all.
dojo.query
With those somewhat surprising findings in hand, it was time to build a better mouse trap. The result is dojo.query, an experimental module that is currently checked in to the Dojo trunk. The system has nearly zero Dojo dependencies and can therefore be safely dropped into older Dojo environments (perhaps even pre 0.4) without difficulty.
The API is straightforward:
// include the system
dojo.require('dojo.query');
// run some queries
dojo.query('#id');
dojo.query('div:first-child');
dojo.query('code.example');
dojo.query('.example');
All queries return an array of unique DOM nodes. Today this is just a "raw" array as I'm investigating what extensions make sense on the return object and what should instead be deferred to a layer on top of dojo.query such as dojo.behavior.
Today dojo.query only supports CSS 1-3 queries although support may be added for xpath-ish semantics if there's sufficient demand and I can justify the engineering time based on some hope that we can make what we do work on whatever fast path browsers eventually provide. The worst possible outcome would be to provided an interface that that browsers almost make obsolete.
How fast is "fast enough"?
Using the the test suite from DomQuery (but with a Dojo-based test runner system), we can show the relative performance of DomQuery, jQuery, and dojo.query. While clearly the slowest of the bunch, jQuery 1.1 provides the richest API today and there's no telling how providing a richer return object may affect the relative timings of dojo.query on the tests where all 3 systems are relatively close. In any case, these tests show raw performance of the node query engine and not much else. You can obviously write fast apps with a slow engine by using it less and write slow apps with a fast engine by writing obscene queries or doing expensive operations on the return.
But enough jibba-jabba. Gimmie data!:
IE 7:
| # | Test | Iterations | Dojo | YUI.ext | JQuery 1.1 |
|---|---|---|---|---|---|
| 1 | div span span | 50 | 360 | 431 | 1612 |
| 2 | span span div | 50 | 290 | 331 | 1432 |
| 3 | #test-data2 span span div | 50 | 80 | 70 | 90 |
| 4 | h4 | 10 | 40 | 120 | 50 |
| 5 | h4.thinger | 10 | 61 | 71 | 80 |
| 6 | .thinger | 10 | 240 | 200 | 320 |
| 7 | .thinger2 | 10 | 230 | 200 | 321 |
| 8 | #test-data | 50 | 20 | 20 | 10 |
| 9 | div#test-data | 50 | 20 | 40 | 0 |
| 10 | span#test-data | 50 | 10 | 80 | 20 |
| 11 | #test-data span | 50 | 160 | 320 | 151 |
| 12 | #test-data pre code | 50 | 40 | 20 | 40 |
| 13 | #test-data pre > code | 10 | 10 | 10 | 10 |
| 14 | #test-data pre code span | 10 | 30 | 70 | 140 |
| 15 | #test-data pre > code > span | 10 | 80 | 50 | 40 |
| 16 | #test-data span.hl-code | 50 | 241 | 251 | 201 |
| 17 | #test-data pre.highlighted > code | 50 | 50 | 30 | 10 |
| 18 | #test-data span:first-child | 50 | 240 | 170 | 201 |
| 19 | #test-data span:last-child | 50 | 211 | 211 | 300 |
| 20 | #test-data span:empty | 50 | 321 | 100 | 280 |
| 21 | #test-data span:first | 50 | 40 | 60 | 161 |
| 22 | #test-data span:last | 50 | 40 | 70 | 170 |
| 23 | #test-data span.hl-code, #test-data span.hl-brackets | 50 | 411 | 371 | 571 |
| 24 | #test-data span:nth-child(2) | 50 | 280 | 320 | 400 |
| 25 | #test-data span:nth-child(3) | 50 | 221 | 321 | 401 |
| 26 | #test-data span:nth-child(0n+3) | 50 | 250 | 4507 | 6853 |
| 27 | #test-data span:nth-child(even) | 10 | 130 | 70 | 1221 |
| 28 | #test-data span:nth-child(2n) | 10 | 170 | 902 | 1402 |
| 29 | #test-data span:nth-child(odd) | 10 | 121 | 70 | 1252 |
| 30 | #test-data span:nth-child(2n+1) | 10 | 120 | 892 | 1513 |
| 31 | #test-data span:contains(new) | 50 | 541 | 500 | 1543 |
| 32 | #test-data span:not(span.hl-code) | 10 | 120 | 120 | 110 |
| 33 | #test-data :first-child | 50 | 952 | 911 | 1944 |
| 34 | #test-data span.hl-default | 50 | 221 | 191 | 230 |
| 35 | #test-data span:not(:first-child) | 50 | 541 | 560 | 451 |
| 36 | #test-data2 div:last-child | 50 | 20 | 20 | 30 |
| 37 | #test-data2 code#inner1 code#inner2 | 50 | 20 | 10 | 30 |
| 38 | #test-data span.hl-default:not(:first-child) | 50 | 320 | 190 | 290 |
| 39 | #test-data span[title] | 50 | 110 | 180 | 2004 |
| 40 | #test-data span[title=east] | 50 | 250 | 211 | 3386 |
| 41 | #test-data span[title="east"] | 50 | 200 | 221 | 3064 |
| 42 | #test-data span[@title="east"] | 50 | 230 | 200 | 681 |
| 43 | #test-data span[title!=east] | 50 | 500 | 520 | 3024 |
| 44 | #test-data span[title^=min] | 50 | 290 | 221 | 2983 |
| 45 | #test-data span[title$=er] | 50 | 271 | 200 | 2983 |
| 46 | #test-data span[title*=in] | 50 | 280 | 200 | 3014 |
IE 6
| # | Test | Iterations | Dojo | YUI.ext | JQuery 1.1 |
|---|---|---|---|---|---|
| 1 | div span span | 50 | 1102 | 831 | 2653 |
| 2 | span span div | 50 | 681 | 621 | 2363 |
| 3 | #test-data2 span span div | 50 | 161 | 200 | 251 |
| 4 | h4 | 10 | 110 | 421 | 110 |
| 5 | h4.thinger | 10 | 160 | 210 | 271 |
| 6 | .thinger | 10 | 611 | 651 | 551 |
| 7 | .thinger2 | 10 | 771 | 641 | 1012 |
| 8 | #test-data | 50 | 40 | 61 | 60 |
| 9 | div#test-data | 50 | 40 | 150 | 70 |
| 10 | span#test-data | 50 | 40 | 371 | 60 |
| 11 | #test-data span | 50 | 401 | 871 | 580 |
| 12 | #test-data pre code | 50 | 120 | 80 | 160 |
| 13 | #test-data pre > code | 10 | 40 | 20 | 50 |
| 14 | #test-data pre code span | 10 | 130 | 211 | 351 |
| 15 | #test-data pre > code > span | 10 | 220 | 170 | 171 |
| 16 | #test-data span.hl-code | 50 | 761 | 650 | 802 |
| 17 | #test-data pre.highlighted > code | 50 | 161 | 120 | 250 |
| 18 | #test-data span:first-child | 50 | 750 | 691 | 1302 |
| 19 | #test-data span:last-child | 50 | 681 | 710 | 1182 |
| 20 | #test-data span:empty | 50 | 962 | 541 | 972 |
| 21 | #test-data span:first | 50 | 110 | 271 | 751 |
| 22 | #test-data span:last | 50 | 91 | 260 | 762 |
| 23 | #test-data span.hl-code, #test-data span.hl-brackets | 50 | 1052 | 1283 | 1913 |
| 24 | #test-data span:nth-child(2) | 50 | 922 | 1032 | 1683 |
| 25 | #test-data span:nth-child(3) | 50 | 790 | 1032 | 1933 |
| 26 | #test-data span:nth-child(0n+3) | 50 | 640 | 10265 | 17520 |
| 27 | #test-data span:nth-child(even) | 10 | 340 | 151 | 3044 |
| 28 | #test-data span:nth-child(2n) | 10 | 470 | 2213 | 3966 |
| 29 | #test-data span:nth-child(odd) | 10 | 310 | 260 | 2784 |
| 30 | #test-data span:nth-child(2n+1) | 10 | 390 | 2303 | 4777 |
| 31 | #test-data span:contains(new) | 50 | 1110 | 1002 | 3755 |
| 32 | #test-data span:not(span.hl-code) | 10 | 371 | 461 | 311 |
| 33 | #test-data :first-child | 50 | 2725 | 3015 | 5599 |
| 34 | #test-data span.hl-default | 50 | 640 | 551 | 681 |
| 35 | #test-data span:not(:first-child) | 50 | 1272 | 1473 | 1462 |
| 36 | #test-data2 div:last-child | 50 | 100 | 100 | 261 |
| 37 | #test-data2 code#inner1 code#inner2 | 50 | 70 | 100 | 341 |
| 38 | #test-data span.hl-default:not(:first-child) | 50 | 912 | 580 | 1193 |
| 39 | #test-data span[title] | 50 | 650 | 721 | 5740 |
| 40 | #test-data span[title=east] | 50 | 971 | 891 | 7926 |
| 41 | #test-data span[title="east"] | 50 | 1021 | 941 | 8604 |
| 42 | #test-data span[@title="east"] | 50 | 1051 | 901 | 2413 |
| 43 | #test-data span[title!=east] | 50 | 1452 | 1742 | 7450 |
| 44 | #test-data span[title^=min] | 50 | 1083 | 841 | 7762 |
| 45 | #test-data span[title$=er] | 50 | 1061 | 921 | 8463 |
| 46 | #test-data span[title*=in] | 50 | 982 | 891 | 8664 |
Firefox 2:
| # | Test | Iterations | Dojo | YUI.ext | JQuery 1.1 |
|---|---|---|---|---|---|
| 1 | div span span | 50 | 171 | 350 | 1302 |
| 2 | span span div | 50 | 90 | 351 | 1493 |
| 3 | #test-data2 span span div | 50 | 120 | 80 | 90 |
| 4 | h4 | 10 | 20 | 70 | 40 |
| 5 | h4.thinger | 10 | 70 | 71 | 80 |
| 6 | .thinger | 10 | 50 | 230 | 461 |
| 7 | .thinger2 | 10 | 50 | 220 | 681 |
| 8 | #test-data | 50 | 0 | 0 | 70 |
| 9 | div#test-data | 50 | 10 | 150 | 60 |
| 10 | span#test-data | 50 | 0 | 80 | 70 |
| 11 | #test-data span | 50 | 110 | 180 | 110 |
| 12 | #test-data pre code | 50 | 160 | 20 | 30 |
| 13 | #test-data pre > code | 10 | 30 | 0 | 0 |
| 14 | #test-data pre code span | 10 | 50 | 40 | 141 |
| 15 | #test-data pre > code > span | 10 | 40 | 40 | 40 |
| 16 | #test-data span.hl-code | 50 | 221 | 200 | 190 |
| 17 | #test-data pre.highlighted > code | 50 | 160 | 10 | 50 |
| 18 | #test-data span:first-child | 50 | 260 | 190 | 451 |
| 19 | #test-data span:last-child | 50 | 240 | 210 | 340 |
| 20 | #test-data span:empty | 50 | 321 | 211 | 180 |
| 21 | #test-data span:first | 50 | 20 | 30 | 220 |
| 22 | #test-data span:last | 50 | 20 | 40 | 130 |
| 23 | #test-data span.hl-code, #test-data span.hl-brackets | 50 | 441 | 360 | 491 |
| 24 | #test-data span:nth-child(2) | 50 | 240 | 300 | 611 |
| 25 | #test-data span:nth-child(3) | 50 | 200 | 440 | 501 |
| 26 | #test-data span:nth-child(0n+3) | 50 | 201 | 8392 | 8162 |
| 27 | #test-data span:nth-child(even) | 10 | 80 | 70 | 1332 |
| 28 | #test-data span:nth-child(2n) | 10 | 90 | 1733 | 1683 |
| 29 | #test-data span:nth-child(odd) | 10 | 70 | 61 | 1332 |
| 30 | #test-data span:nth-child(2n+1) | 10 | 90 | 1632 | 1603 |
| 31 | #test-data span:contains(new) | 50 | 311 | 292 | 1623 |
| 32 | #test-data span:not(span.hl-code) | 10 | 110 | 100 | 60 |
| 33 | #test-data :first-child | 50 | 1693 | 1342 | 1983 |
| 34 | #test-data span.hl-default | 50 | 210 | 171 | 270 |
| 35 | #test-data span:not(:first-child) | 50 | 431 | 411 | 362 |
| 36 | #test-data2 div:last-child | 50 | 0 | 10 | 70 |
| 37 | #test-data2 code#inner1 code#inner2 | 50 | 160 | 0 | 50 |
| 38 | #test-data span.hl-default:not(:first-child) | 50 | 331 | 331 | 411 |
| 39 | #test-data span[title] | 50 | 190 | 160 | 1941 |
| 40 | #test-data span[title=east] | 50 | 191 | 170 | 3105 |
| 41 | #test-data span[title="east"] | 50 | 190 | 170 | 3065 |
| 42 | #test-data span[@title="east"] | 50 | 190 | 170 | 465 |
| 43 | #test-data span[title!=east] | 50 | 370 | 340 | 3025 |
| 44 | #test-data span[title^=min] | 50 | 200 | 140 | 3134 |
| 45 | #test-data span[title$=er] | 50 | 210 | 171 | 3124 |
| 46 | #test-data span[title*=in] | 50 | 220 | 170 | 3133 |
Safari 2.0:
| # | Test | Iterations | Dojo | YUI.ext | JQuery 1.1 |
|---|---|---|---|---|---|
| 1 | div span span | 50 | 864 | 1401 | 4952 |
| 2 | span span div | 50 | 844 | 1258 | 5477 |
| 3 | #test-data2 span span div | 50 | 291 | 356 | 707 |
| 4 | h4 | 10 | 204 | 539 | 351 |
| 5 | h4.thinger | 10 | 293 | 359 | 500 |
| 6 | .thinger | 10 | 1011 | 840 | 1626 |
| 7 | .thinger2 | 10 | 948 | 851 | 1626 |
| 8 | #test-data | 50 | 4 | 40 | 94 |
| 9 | div#test-data | 50 | 35 | 110 | 83 |
| 10 | span#test-data | 50 | 36 | 360 | 95 |
| 11 | #test-data span | 50 | 597 | 1323 | 997 |
| 12 | #test-data pre code | 50 | 95 | 77 | 348 |
| 13 | #test-data pre > code | 10 | 40 | 21 | 50 |
| 14 | #test-data pre code span | 10 | 141 | 290 | 361 |
| 15 | #test-data pre > code > span | 10 | 233 | 193 | 311 |
| 16 | #test-data span.hl-code | 50 | 843 | 889 | 1373 |
| 17 | #test-data pre.highlighted > code | 50 | 189 | 101 | 340 |
| 18 | #test-data span:first-child | 50 | 445 | 575 | 1303 |
| 19 | #test-data span:last-child | 50 | 449 | 585 | 1321 |
| 20 | #test-data span:empty | 50 | 1103 | 253 | 969 |
| 21 | #test-data span:first | 50 | 60 | 65 | 883 |
| 22 | #test-data span:last | 50 | 93 | 93 | 865 |
| 23 | #test-data span.hl-code, #test-data span.hl-brackets | 50 | 1571 | 1471 | 2775 |
| 24 | #test-data span:nth-child(2) | 50 | 467 | 728 | 1695 |
| 25 | #test-data span:nth-child(3) | 50 | 443 | 704 | 2028 |
| 26 | #test-data span:nth-child(0n+3) | 50 | 420 | 2950 | 31899 |
| 27 | #test-data span:nth-child(even) | 10 | 504 | 290 | 5006 |
| 28 | #test-data span:nth-child(2n) | 10 | 549 | 627 | 6330 |
| 29 | #test-data span:nth-child(odd) | 10 | 498 | 276 | 4906 |
| 30 | #test-data span:nth-child(2n+1) | 10 | 519 | 597 | 6399 |
| 31 | #test-data span:contains(new) | 50 | 846 | 799 | 5628 |
| 32 | #test-data span:not(span.hl-code) | 10 | 493 | 644 | 519 |
| 33 | #test-data :first-child | 50 | 2032 | 2712 | 7656 |
| 34 | #test-data span.hl-default | 50 | 721 | 641 | 1267 |
| 35 | #test-data span:not(:first-child) | 50 | 1092 | 2312 | 1779 |
| 36 | #test-data2 div:last-child | 50 | 106 | 120 | 375 |
| 37 | #test-data2 code#inner1 code#inner2 | 50 | 81 | 100 | 382 |
| 38 | #test-data span.hl-default:not(:first-child) | 50 | 1047 | 682 | 1425 |
| 39 | #test-data span[title] | 50 | 299 | 617 | 17526 |
| 40 | #test-data span[title=east] | 50 | 983 | 698 | 33624 |
| 41 | #test-data span[title="east"] | 50 | 981 | 709 | 33776 |
| 42 | #test-data span[@title="east"] | 50 | 968 | 697 | 4476 |
| 43 | #test-data span[title!=east] | 50 | 1466 | 1901 | 33950 |
| 44 | #test-data span[title^=min] | 50 | 1336 | 725 | 33928 |
| 45 | #test-data span[title$=er] | 50 | 1939 | 758 | 33977 |
| 46 | #test-data span[title*=in] | 50 | 1341 | 701 | 33643 |
Webkit Nightly (aka, "Leopard Safari"):
| # | Test | Iterations | Dojo | YUI.ext | JQuery 1.1 |
|---|---|---|---|---|---|
| 1 | div span span | 50 | 113 | 170 | 516 |
| 2 | span span div | 50 | 119 | 165 | 515 |
| 3 | #test-data2 span span div | 50 | 33 | 54 | 128 |
| 4 | h4 | 10 | 19 | 40 | 44 |
| 5 | h4.thinger | 10 | 45 | 59 | 70 |
| 6 | .thinger | 10 | 418 | 428 | 487 |
| 7 | .thinger2 | 10 | 164 | 164 | 236 |
| 8 | #test-data | 50 | 2 | 16 | 24 |
| 9 | div#test-data | 50 | 5 | 31 | 26 |
| 10 | span#test-data | 50 | 1 | 54 | 22 |
| 11 | #test-data span | 50 | 66 | 125 | 144 |
| 12 | #test-data pre code | 50 | 14 | 31 | 82 |
| 13 | #test-data pre > code | 10 | 4 | 7 | 20 |
| 14 | #test-data pre code span | 10 | 17 | 25 | 59 |
| 15 | #test-data pre > code > span | 10 | 32 | 27 | 50 |
| 16 | #test-data span.hl-code | 50 | 866 | 906 | 973 |
| 17 | #test-data pre.highlighted > code | 50 | 41 | 37 | 120 |
| 18 | #test-data span:first-child | 50 | 80 | 101 | 226 |
| 19 | #test-data span:last-child | 50 | 79 | 102 | 234 |
| 20 | #test-data span:empty | 50 | 132 | 74 | 183 |
| 21 | #test-data span:first | 50 | 20 | 43 | 145 |
| 22 | #test-data span:last | 50 | 19 | 37 | 146 |
| 23 | #test-data span.hl-code, #test-data span.hl-brackets | 50 | 1069 | 1113 | 1417 |
| 24 | #test-data span:nth-child(2) | 50 | 92 | 148 | 340 |
| 25 | #test-data span:nth-child(3) | 50 | 83 | 136 | 245 |
| 26 | #test-data span:nth-child(0n+3) | 50 | 78 | 1143 | 1772 |
| 27 | #test-data span:nth-child(even) | 10 | 42 | 31 | 306 |
| 28 | #test-data span:nth-child(2n) | 10 | 45 | 260 | 375 |
| 29 | #test-data span:nth-child(odd) | 10 | 39 | 30 | 307 |
| 30 | #test-data span:nth-child(2n+1) | 10 | 50 | 237 | 360 |
| 31 | #test-data span:contains(new) | 50 | 149 | 154 | 535 |
| 32 | #test-data span:not(span.hl-code) | 10 | 219 | 218 | 67 |
| 33 | #test-data :first-child | 50 | 383 | 385 | 867 |
| 34 | #test-data span.hl-default | 50 | 551 | 570 | 655 |
| 35 | #test-data span:not(:first-child) | 50 | 169 | 231 | 293 |
| 36 | #test-data2 div:last-child | 50 | 17 | 31 | 92 |
| 37 | #test-data2 code#inner1 code#inner2 | 50 | 10 | 44 | 136 |
| 38 | #test-data span.hl-default:not(:first-child) | 50 | 611 | 589 | 680 |
| 39 | #test-data span[title] | 50 | 76 | 95 | 2600 |
| 40 | #test-data span[title=east] | 50 | 115 | 118 | 6782 |
| 41 | #test-data span[title="east"] | 50 | 110 | 131 | 6888 |
| 42 | #test-data span[@title="east"] | 50 | 118 | 124 | 362 |
| 43 | #test-data span[title!=east] | 50 | 199 | 203 | 6641 |
| 44 | #test-data span[title^=min] | 50 | 134 | 113 | 6316 |
| 45 | #test-data span[title$=er] | 50 | 157 | 119 | 6060 |
| 46 | #test-data span[title*=in] | 50 | 134 | 112 | 5734 |
Opera 9:
| # | Test | Iterations | Dojo | YUI.ext | JQuery 1.1 |
|---|---|---|---|---|---|
| 1 | div span span | 50 | 100 | 250 | 800 |
| 2 | span span div | 50 | 50 | 191 | 742 |
| 3 | #test-data2 span span div | 50 | 50 | 40 | 100 |
| 4 | h4 | 10 | 20 | 40 | 20 |
| 5 | h4.thinger | 10 | 30 | 30 | 50 |
| 6 | .thinger | 10 | 70 | 110 | 151 |
| 7 | .thinger2 | 10 | 70 | 101 | 220 |
| 8 | #test-data | 50 | 0 | 0 | 10 |
| 9 | div#test-data | 50 | 0 | 30 | 0 |
| 10 | span#test-data | 50 | 0 | 80 | 20 |
| 11 | #test-data span | 50 | 70 | 140 | 110 |
| 12 | #test-data pre code | 50 | 60 | 30 | 50 |
| 13 | #test-data pre > code | 10 | 20 | 10 | 20 |
| 14 | #test-data pre code span | 10 | 20 | 30 | 60 |
| 15 | #test-data pre > code > span | 10 | 20 | 20 | 30 |
| 16 | #test-data span.hl-code | 50 | 131 | 130 | 200 |
| 17 | #test-data pre.highlighted > code | 50 | 80 | 50 | 70 |
| 18 | #test-data span:first-child | 50 | 30 | 20 | 150 |
| 19 | #test-data span:last-child | 50 | 0 | 20 | 130 |
| 20 | #test-data span:empty | 50 | 0 | 20 | 130 |
| 21 | #test-data span:first | 50 | 10 | 40 | 90 |
| 22 | #test-data span:last | 50 | 30 | 40 | 90 |
| 23 | #test-data span.hl-code, #test-data span.hl-brackets | 50 | 260 | 231 | 381 |
| 24 | #test-data span:nth-child(2) | 50 | 40 | 40 | 110 |
| 25 | #test-data span:nth-child(3) | 50 | 50 | 100 | 140 |
| 26 | #test-data span:nth-child(0n+3) | 50 | 50 | 891 | 1171 |
| 27 | #test-data span:nth-child(even) | 10 | 10 | 20 | 210 |
| 28 | #test-data span:nth-child(2n) | 10 | 0 | 190 | 220 |
| 29 | #test-data span:nth-child(odd) | 10 | 10 | 20 | 231 |
| 30 | #test-data span:nth-child(2n+1) | 10 | 0 | 171 | 230 |
| 31 | #test-data span:contains(new) | 50 | 81 | 110 | 482 |
| 32 | #test-data span:not(span.hl-code) | 10 | 50 | 80 | 30 |
| 33 | #test-data :first-child | 50 | 180 | 280 | 510 |
| 34 | #test-data span.hl-default | 50 | 120 | 101 | 170 |
| 35 | #test-data span:not(:first-child) | 50 | 160 | 130 | 160 |
| 36 | #test-data2 div:last-child | 50 | 0 | 0 | 30 |
| 37 | #test-data2 code#inner1 code#inner2 | 50 | 30 | 10 | 50 |
| 38 | #test-data span.hl-default:not(:first-child) | 50 | 200 | 100 | 220 |
| 39 | #test-data span[title] | 50 | 30 | 20 | 1944 |
| 40 | #test-data span[title=east] | 50 | 40 | 30 | 4456 |
| 41 | #test-data span[title="east"] | 50 | 40 | 50 | 4287 |
| 42 | #test-data span[@title="east"] | 50 | 20 | 10 | 250 |
| 43 | #test-data span[title!=east] | 50 | 130 | 130 | 4006 |
| 44 | #test-data span[title^=min] | 50 | 60 | 20 | 3885 |
| 45 | #test-data span[title$=er] | 50 | 40 | 40 | 3996 |
| 46 | #test-data span[title*=in] | 50 | 40 | 40 | 4136 |
So how'd we do? Generally, dojo.query is in the hunt with DomQuery or edges it out for raw DOM iteration. Like Jack's system dojo.query beats jQuery most of the time. dojo.query also shines on Firefox and Opera where asking questions of the DOM that would result in large "node blooms" can be sidestepped with XPath. Queries like ".thinger" and "div span span" help show how dramatic the difference can be.
Lest anyone get the wrong idea about all the "invalid" attribute tests in jQuery toward the bottom, the syntax used in the tests is generally the CSS selector variant of something that jQuery can probably support in another form. In most cases it would be possible to re-write the test to use a jQuery friendly selector to achieve the same thing.
Interestingly, it's clear that Opera and the Webkit Nightlies beat IE and Firefox for raw query performance across the board. DOM or XPath, ripping through node collections on Opera is blindingly fast. The currently shipping Safari is a dog, though. IE 6 also comes in with numbers that make IE 7's relatively pokey turnout look stellar. At somewhere between 2x and 3x faster, IE 7 is quite the improvement. It might even be something that Microsoft could crow about if IE's Open Source competition didn't make it look so feeble. Firefox isn't the fastest of the lot, but it still takes IE 7 to the woodshed on nearly every test. The gloves will really come off when the Webkit nightlies finally ship in official form.
Running all 3 libraries through the test system created by John Resig, we get results which are broadly in line. You can try them out for yourself. These larger/longer tests are here or on John's smaller test suite is here. Be warned that the test pages pull in a lot of script and do a lot of work. They can easily bog down your box for a good long while.
Getting The Code
dojo.query isn't going to be part of the 0.4.x line, and it's marked experimental for the time being. While I believe it to be correct for the cases it handles, the devil in systems like this is in the details. Furthermore, I'd like it to give more explicit guidance about when it encounters queries that it can't handle before calling it production ready.
You can get a ShrinkSafe'd copy of the library here but be sure to check back with what's in SVN frequently to get the latest, most correct version.
Also, remember that you can include a file like this with either a <script> tag somewhere after your main dojo.js file or with the usual dojo.require syntax. Either way, dropping dojo.query into your app is a snap.
It's Closures All The Way Down
I've kind of glossed over the details of how to make a system like this scream. Having a goal is the first step and being data-driven in optimization is the second. Jack Slockum's excellent DomQuery does a masterful job in reducing dominating factors. From not allocating intermediate arrays in hard cases to "compiling" down functions for future use on a particular query, DomQuery sets the bar very high. In many cases, matching or beating Jack's system proved to be very difficult and for some of the newer code in dojo.query (e.g., attribute selectors) there's still some way to go. With iterative development to tune performance a must, a couple of basic design decisions made evolving the system to meet performance targets easier.
At it's core, dojo.query uses closures as a way to keep query parsing from happening more than once. Instead of naively parsing and running the query each time or even creating a custom function from a string, dojo.query builds up a set of filtering functions in a functional programming style. The result is a system that can count on closure scope for caching intermediate variables of all sorts. It may not be a huge performance win, but the system "feels" more like idiomatic JavaScript and it's always possible to get line numbers when debugging problems. Explicit caching happens only at a coarse level. Full queries and filtering functions for query parts are the biggest recipients of direct memoization. Having the rest of the system be composed of functions that just generate functions simplifies the system greatly. Which is good, because adding another branch (XPath) adds some complexity.
As noted earlier, XPath turns out not to be a "win" on every query, even on the browsers where it can make a huge difference. To handle this, dojo.query adds a heuristic layer to the first-run parsing to figure out whether or not a query can and should be deferred to XPath. Because we can't plan for every query "shape" in the pre-processor and still be space-efficient, the heuristic is rather rough but in the future it will likely improve to handle cases where today it "punts" to regular DOM iteration. This kind of primordial "query optimizer" is likely going to be the focus of some continued development effort since anything we can improve in its heuristic is potentially a huge gain on browsers that give us a fast path. At the very least, the heuristic/optimizer layer gives us a straightforward place to plug in new back-ends when better browser fast-paths emerge or when the relative XPath/DOM performance metrics change between revs of the same browser. For instance, while the Webkit nightlies are very fast and support XPath, regular-old DOM traversal is relatively much faster on that browser than XPath. The optimizer layer lets us take that into account and do the "Right Thing" more often.
While there is some thrill in the hunt of building systems like this that are fast enough to enable fundamental changes in the way Dojo developers work, I wish it weren't necessary. The browser vendors are continuing to let us down, and every year that goes by without fast-path DOM queries, a unified behavior layer, real and portable parametric CSS, socket-like network connections, and unified 2D graphics support is opportunity lost. Sadly, while the IE team shoulders much of the blame for not shipping many of these features in any form, they're not alone in their culpability. I'm proud of dojo.query, but I wish I didn't have to be. Yet another chunk of useful javascript that will be shipped around the web ad-infinitum instead of being baked into the platform itself should make everyone sad.
Lastly, research and development for dojo.query is thanks to SitePen. If you like what Dojo and dojo.query are about, you'll love what SitePen can do (and help you do), for your applications.

Good work! I'm pleased to
This is great stuff. The
Hey Dean, The "+" and "~"
[...] I finally blogged the