Why is getElementsByTagName() faster than querySelectorAll()?
Yesterday, fellow Yahoo and SoundManager creator Scott Schiller expressed some confusion on Twitter over why getElementsByTagName("a") is faster than querySelectorAll("a") in nearly all browsers. There a JSPerf test comparing the two and you can that the speed comparison is fairly pronounced. In the browser I’m using right now, Firefox 3.6.8 on Windows XP, querySelectorAll("a") is a shocking 98% slower than getElementsByTagName("a"). There was a lively Twitter-sation between myself, Scott, and YUI team member Ryan Grove about why this and how disappointing but not unexpected this really is. I thought I’d follow up with a longer description of why exactly this happens and why it probably won’t change very much.
Before digging into details, there’s one very important difference between these two methods, and it’s not that one accepts only a tag name and the other accepts a full CSS selector. The big difference is in the return value: the getElementsByTagName() method returns a live NodeList while querySelectorAll() returns a static NodeList. This is extremely important to understand.
Live NodeLists
This is one of the major gotchas of the Document Object Model. The NodeList object (also, the HTMLCollection object in the HTML DOM) is a special type of object. The DOM Level 3 spec says about HTMLCollection objects:
NodeListandNamedNodeMapobjects in the DOM are live; that is, changes to the underlying document structure are reflected in all relevantNodeListandNamedNodeMapobjects. For example, if a DOM user gets aNodeListobject containing the children of anElement, then subsequently adds more children to that element (or removes children, or modifies them), those changes are automatically reflected in theNodeList, without further action on the user’s part. Likewise, changes to aNodein the tree are reflected in all references to thatNodeinNodeListandNamedNodeMapobjects.
The getElementsByTagName() method returns one of these live collections of elements that are automatically updated whenever the document is changed. Thus, the following is actually an infinite loop:
var divs = document.getElementsByTagName("div"),
i=0;
while(i < divs.length){
document.body.appendChild(document.createElement("div"));
i++;
}
The infinite loop occurs because divs.length is recalculated each time through the loop. Since each iteration of the loop is adding a new <div>, which means divs.length is being incremented each time through the loop so i, which is also being incremented, can never catch up and terminal condition is never triggered.
These live collections might seems like a bad idea, but they are in place to enable the same objects to be used for document.images, document.forms, and other similar pre-DOM collections that had become commonplace in browsers.
Static NodeLists
The querySelectorAll() method is different because it is a static NodeList instead of a live one. This is indicated in the Selectors API spec:
The
NodeListobject returned by thequerySelectorAll()method must be static, not live ([DOM-LEVEL-3-CORE], section 1.1.1). Subsequent changes to the structure of the underlying document must not be reflected in theNodeListobject. This means that the object will instead contain a list of matchingElementnodes that were in the document at the time the list was created.
So even though the return value of querySelectorAll() has the same methods and behaves the same as those returned by getElementsByTagName(), they are actually very different. In the former case, the NodeList is a snapshot of the document’s state at the time the method was called whereas the latter case will always be up to date with the current state of the document. This is not an infinite loop:
var divs = document.querySelectorAll("div"),
i=0;
while(i < divs.length){
document.body.appendChild(document.createElement("div"));
i++;
}
There is no infinite loop in this case. The value of divs.length never changes, so the loop will essentially double the number of <div> elements in the document and then exit.
So why are live NodeLists faster?
Live NodeList objects can be created and returned faster by the browser because they don’t have to have all of the information up front while static NodeLists need to have all of their data from the start. To hammer home the point, the WebKit source code has a separate source file for each type of NodeList: DynamicNodeList.cpp and StaticNodeList.cpp. The two object types are created in very different ways.
The DynamicNodeList object is created by registering its existence in a cache. Essentially, the overheard to creating a new DynamicNodeList is incredibly small because it doesn’t have to do any work upfront. Whenever the DynamicNodeList is accessed, it must query the document for changes, as evidenced by the length property and the item() method (which is the same as using bracket notation).
Compare this to the StaticNodeList object, instances of which are created in another file and then populated with all of the data inside of a loop. The upfront cost to running a query on the document is much more significant than when using a DynamicNodeList instance.
If you take a look at the WebKit source code that actually creates the return value for querySelectorAll(), you’ll see that a loop is used to get every result and build up a NodeList that is eventually returned.
Conclusion
The real reason why getElementsByTagName() is faster than querySelectorAll() is because of the difference between live and static NodeList objects. Although I’m sure there are way to optimize this, doing no upfront work for a live NodeList will generally always be faster than doing all of the work to create a static NodeList. Determining which method to use is highly dependent on what you’re trying to do. If you’re just searching for elements by tag name and you don’t need a snapshot, then getElementsByTagName() should be used; if you do need a snapshot of results or you’re doing a more complex CSS query, then querySelectorAll() should be used.
Disclaimer: Any viewpoints and opinions expressed in this article are those of Nicholas C. Zakas and do not, in any way, reflect those of my employer, my colleagues, Wrox Publishing, O'Reilly Publishing, or anyone else. I speak only for myself, not for them.
Both comments and pings are currently closed.




20 Comments
Seems like we’re trading the “work” (and thus the time) of when that DOM calculation happens… for QSA, it happens at call time and a static node list is returned with the time penalty already paid. But with gEBTN, we’re getting a quicker live-node list reference, but each time it’s accessed, the DOM has to be re-queried and re-calculated. Like gov’t taxes: “pay me now, or pay me later”.
I’d be interested to see if there’s any real time savings when factoring in the DOM access and re-querying (for a reasonable use-case) as compared to getting a static node list that you can access/change all you want without paying DOM access penalty time.
This is just a special perspective of the oft-cited performance advice (usually in the jquery world) of operating on collections of objects (in dom fragments even) and then inserting the whole collection, rather than one at a time.
Good stuff, thanks for the article!
Kyle Simpson on September 28th, 2010 at 8:21 pm
Good article. I agree with kyle that it would be interesting to see the performance differences when iterating over the static and dynamic NodeList objects.
Adam Platti on September 28th, 2010 at 8:41 pm
Since the difference in speed comes from the underlying difference in how static NodeList and Live NodeList operate, this would also mean that the native document.getElementsByClassName(which returns a live NodeList) in the browsers would also be faster than document.querySelectorAll for the same reason, right ?
Rajat on September 29th, 2010 at 12:02 am
Recognizing the difference between the live vs. static results list is important, I completely glossed over that when imagining how the two methods work from the selector engine approach.
For what it’s worth, I was looking into this as I’d never tried querySelectorAll, and was wondering if I should refactor some of the old SM2 demos which use getElementsByTagName, to fetch certain elements – so after all of that, it appears that ByTagName() is appropriate for my given use case.
Interestingly, according to the reports collected on the speed test page, Opera was actually faster using querySelectorAll.
Scott Schiller on September 29th, 2010 at 2:16 am
This reminds be of some work Diego Perini was doing that suggested converting node lists to arrays before iterating was faster. http://jsperf.com/nodelist-vs-array-iteration
John-David Dalton on September 29th, 2010 at 2:51 am
Well, that’s not enough to explain the difference. You’ll find my own take about it here: http://is.gd/fzj4y
Daniel Glazman on September 29th, 2010 at 3:38 am
I forked the JSPerf test to see if there is differences in the access to properties of a Live list or static list :
http://jsperf.com/access-to-nodes-via-queryselectorall-vs-getelementsbyta/2
the results varies depending on the browser, but it seems that with just an access and a write of the .href property (note that I cached the .length property), the getElementsByTagName is still quicker but not that much, so there is a bit of compensation
jpvincent on September 29th, 2010 at 3:41 am
Iterating through all members of the list will not be sufficient – as Daniel Glazman mentioned in his reply (http://www.glazman.org/weblog/dotclear/index.php?post/2010/09/29/Why-is-getElementsByTagName-faster-that-querySelectorAll), results of getElementsByTagName are easy to cache so most browsers probably do cache them. So for multiple subsequent measurements this cache needs to be invalidated every time – e.g. by doing something like document.body.innerHTML = document.body.innerHTML.
Wladimir Palant on September 29th, 2010 at 6:06 am
@Rajat – Yes, that’s correct.
@Daniel – I may be over simplifying here, but I’d think parsing one token wouldn’t be very expensive in and of itself (especially since it’s done only once), and then applying that simple pattern match on nodes also wouldn’t be expensive. I can see more complex CSS queries ending up taking more time, but it seems like there should be optimizations for queries that essentially act like other already available DOM methods. Even so, granting you that the cost is higher of making a querySelectorAll() call, changing it so that both methods had a theoretical implementation with the same amount of overhead, the method using the live NodeList would always return faster.
@Wladimir – Yup, browsers definitely cache the results (take a look at the source code I referenced from WebKit) and the link that Daniel provided.
Nicholas C. Zakas on September 29th, 2010 at 11:15 am
Great post.
Took me quite some time to get this while debugging some native DOM code.
I also wrote a post about this a while ago: http://devign.me/getelementsbytagname-is-always-up-to-date-according-to-the-current-dom-tree/
Elad Ossadon on September 29th, 2010 at 1:53 pm
@Nicholas, I don’t mean to stray from the point of your article which is plain and simple: getElementsBy[xxxx] is generally faster than querySelectorAll(xxxx). Unless you need to be using querySelectorAll, you probably shouldn’t.
That said, @jpvincent brings up an excellent point w/ his fork. The overall performance difference should be much smaller once you start accessing the nodes in the list.
Let’s first assume that our lovable browsers have a 100% optimized querySelectorAll in terms of parsing selectors and looking up those selects. In reality this will hardly be the case, but it will allow us to focus purely on the live vs static NodeList. Thus, we will claim f to be the time it takes to find/select all nodes to be used in either type of NodeList.
Let L be the total time for the initial NodeList lookup. Accessing a static NodeList of n elements would then take time L=f+(s*n) (where s is equivalent to the time necessary to take a “snapshot” of a given node.) A live NodeList of n elements would take time L=f (essentially direct access.) Obviously this is a huge improvement as you stated.
Now let A be the total time to access all elements necessary within a NodeList. If you were then to access just one of those elements out of a live NodeList you would have to make just one “snapshot”; you would get time A=L+s*1. If we let m be the number of elements we actually need to access in the returned NodeList, then the equation actually becomes A=L+s*m. The static NodeList would have A=L+m since the snapshots were already created.
Expanding these equations we get:
live: A=f+(s*m)
static: A=f+(s*n)+m
As m approaches n, the equations start to normalize to:
live: A=f+s*n
static: A=f+(s*n)+n …or… A=f+(s+1)n
The +1 is essentially caused by the second loop necessary to access the object/data out of the static NodeList vs the live NodeList which we’ve assumed to not loop at all during its initial lookup. However you may want to consider the value of s; if it is a large value (significant enough to cause a noticeable degrade performance,) adding +1 would leave the difference between the getElementsByTagName and querySelectorAll as negligible.
The big difference in performance here lies w/ m (the number of elements in the NodeList to be accessed.) Looping through all div tags on the page returned by getElementByTagName will likely only reward you with slightly better performance than using querySelectorAll. Looping through only the first 3 div tags returned by getElementsByTagName will certainly reward you with a significant performance increase.
Jacob Swartwood on September 29th, 2010 at 2:28 pm
[...] Why is getElementsByTagName() faster that querySelectorAll()? Nicholas Zakas on why querySelectorAll is actually slower: Before digging into details, there’s one very important difference between these two methods, and it’s not that one accepts only a tag name and the other accepts a full CSS selector. The big difference is in the return value: the getElementsByTagName() method returns a live NodeList while querySelectorAll() returns a static NodeList. This is extremely important to understand. (tagged: javascript dom queryselectorall getelementsbytagname performance ) [...]
Linkdump for September 30th at found_drama on September 30th, 2010 at 11:02 am
@Jacob – I think there’s a flaw in your logic. There is no cost associated with taking a snapshot of a node. That’s not what a static NodeList does. The nodes themselves are live in the DOM, the static NodeList just contains pointers to those nodes. The snapshot is of the result set, not of an individual node, and therefore there’s no cost to that.
Comparing getElementsByTagName() to querySelectorAll() is really not a good benchmark because the former doesn’t even need to find the first element matching the tag until you start using the live NodeList, effectively deferring any work until later. The latter needs to do all the work up front. It would actually be a better comparison to run getElementsByTagName(“a”)[0] vs. querySelector(“a”). These effectively do the same thing. I’ve setup a JSPerf test here: http://jsperf.com/getelementsbytagname-a-0-vs-queryselector-a. In this, querySelector() still does the upfront work of parsing the CSS selector but only needs to return the first item. In this case it’s likely that caching of the live NodeLists is affecting the relative performance.
Nicholas C. Zakas on September 30th, 2010 at 11:22 am
[...] Why is getElementsByTagName() faster that querySelectorAll()? Nicholas C. Zakas discusses the performance characteristics of the NodeList object for static and [...]
Game Tutorial, has.js, getElementsByTagName Performance | tips & tricks on October 1st, 2010 at 4:17 pm
Just to muddy the waters, there’s a graph on p.49 of “High Performance Javascript” (a book I’m sure you’re familiar with…) that shows that for more complex queries, iterating QSA() will be noticably faster than iterating BTN().
Nick Tulett on October 6th, 2010 at 10:08 am
I would like to point out that while live nodeLists may be the fastest to generate they are the slowest to access elements. http://jsperf.com/nodelist-vs-array-iteration
If storing the results to iterate over repeatedly you should convert them to an array.
John-David Dalton on October 12th, 2010 at 2:49 am
I made a version of the speed test using while loops and Array.slice conversion to arrays (since I was lead to believe these techniques were faster). I would be interested in seeing how this compares to other types of loops and other versions of array construction. That may become another test.
http://jsperf.com/nodelist-vs-array-slice-iteration
Paul Grenier on October 18th, 2010 at 2:55 pm
@Nicholas I agree that there was a flaw in my logic. I believe I misunderstood you original use of the term “snapshot”, and thus my usage was inaccurate; what I meant was more regarding the weight of access time. Those time assumptions were also a little flawed.
Essentially I was trying to expand to say that if you were to use a node, you would eventually have to do more work at the point of acces than with the StaticNodeList (because, as you stated a DynamicNodeList is “essentially deferring any work until later”. http://jsperf.com/nodelist-vs-array-iteration emphasizes that by showing the weight of accessing a node from a DynamicNodeList is greater than that of accessing a node in a StaticNodeList.
I tried two tests to compare performance gains accessing one node vs all nodes.
http://jsperf.com/access-to-nodes-via-queryselectorall-vs-getelementsbyta/5 => ~71% slower
http://jsperf.com/access-to-nodes-via-queryselectorall-vs-getelementsbyta/6 => ~98% slower
…but I think your http://jsperf.com/getelementsbytagname-a-0-vs-queryselector-a might still be a more pure representation of that.
Jacob Swartwood on October 29th, 2010 at 11:02 am
But actually iterating over the results, which is what I think most people are likely to do with the results, switches things around.
In the following test, iterating over QSA results is significantly faster than iterating over GEBTN results:
http://jsperf.com/getelementsbytagname-a-0-vs-queryselector-a/4
Am I missing something fundamental?
Scott Sauyet on December 3rd, 2010 at 5:08 pm
@Scott – Yes, iterating over QSA results will always be faster because it’s a static node list. The GEBTN results are a live nodelist, so it must check each time through the loop whether the DOM has changed.
Nicholas C. Zakas on December 3rd, 2010 at 11:38 pm
Comments are automatically closed after 14 days.