Do you have to furnish a lot of HTML elements from JavaScript, such as for formatting or inserting special effects? Well, then you need to know the performance - or lack thereof - of finding and traversing DOM elements. And the performance hit of using Microsoft's browser.
This post tries to clarify the performance hits involved. Let the game begin!
First of all, let us introduce the players: we have two browsers, MSIE and Firefox. On top of JavaScript we use one library, Prototype. I have discussed some of the downsides of using Prototype to extend JavaScript arrays, but will here focus on the performance hits.
And the field? It is a DOM consisting of 10,000 div elements, fed from an HTML document generated by the following cpsh script, using 10000 as the command-line parameter:
-
#!/usr/bin/cpsh
-
cout <<"<html><head></head>\n<body>\n";
-
. int i;
-
. string genDiv() {
-
. return "<div id=\"" + lexical_cast<string>(++i) +
-
. "\" class=\"test\">test</div>";
-
. }
-
generate_n(ostream_iterator<string>(cout, "\n"),
-
lexical_cast<int>(argv[1]), genDiv);
-
cout <<"</body></html>" <<endl;
After letting this cpsh (C++) script do the hard work, I then inserted the following two script include statements in the head element:
Now, we can start the fun. We have to write JavaScript code to fetch the nodes and then traverse them.
Fetching is done via the getElementsByClassName extension that Prototype brings to the document object. Note: with a lot of elements, relative the number of sought after elements, this function is quite slow, especially on MSIE, so I there suggest using an alternative, getElementsByClassAndTag, which I defined based on that Prototype function; more of that later in this post.
We then loop in two ways: via the JavaScript for ... in construct, and using Prototype's each extension to arrays. Additionally, we allow these loops to actually alter elements - in our case simply changing the innerHTML attribute to done. Thus, we end up with 5 tests.
The code of profile_loops.js follows:
-
// Run the tests
-
function runTests()
-
{
-
var startPoint = new Date().getTime();
-
var elems = getTestElements();
-
var len = elems.length;
-
var fetchedPoint = new Date().getTime();
-
forLoop(elems);
-
var foredPoint = new Date().getTime();
-
eachLoop(elems);
-
var eachedPoint = new Date().getTime();
-
forLoop(elems, true);
-
var doForedPoint = new Date().getTime();
-
eachLoop(elems, true);
-
var doEachedPoint = new Date().getTime();
-
alert("The tests: getting=" +
-
(fetchedPoint - startPoint) +
-
" ms, foring=" +
-
(foredPoint - fetchedPoint) +
-
" ms, each=" +
-
(eachedPoint - foredPoint) +
-
" ms, do foring=" +
-
(doForedPoint - eachedPoint) +
-
" ms, do each=" +
-
(doEachedPoint - doForedPoint));
-
}
-
-
// Fetching the test elements
-
function getTestElements()
-
{
-
return document.getElementsByClassName("test");
-
}
-
-
// Regular for loop
-
function forLoop(elements, doSomething)
-
{
-
for (i in elements) {
-
var elem = elements[i];
-
if (doSomething)
-
elem.innerHTML = "done";
-
}
-
}
-
-
// Prototype's each extension
-
function eachLoop(elements, doSomething)
-
{
-
elements.each( function (elem) {
-
if (doSomething)
-
elem.innerHTML = "done";
-
});
-
}
Note: we actually sneaked in an onload="runTests" in the body element when you were not watching. Sorry.
So, what were the results? No drumrolls, but here they are, per 10,000 iterations/elements, in milliseconds, for both MSIE (6.0) and Firefox (1.5). Wait, before looking at them we have this clear intuition that these five tests are quite similar in complexity, so the times they take will be of the same magnitude. Right?

What happened to those other tests? I can only see get elems with 1 minute for retrieving those 10,000 div:s; and only for MSIE. Was the PNG creation defect so we lost those bars? No. All looping and - simple - HTML altering actions are dwarfed by simply getting those test elements in that very first line of code, document.getElementByClassName. So, one advice is - apparently - to be very careful when using it in MSIE; you might be better off with my suggestion in the bottom of this post. Another advice is to skip MSIE
Again, getting elements via class name takes around 1 ms per element in MSIE. That is about 3 million clock cycles. Per element. Cool
Note: Firefox is about 60 (!) times faster here.
Ok, enough bashing, let us look at those loops up close, removing that first test from the equation:

Here we clearly see that actually doing something to the elements is more expensive that just looping. Not a huge surprise. The actual looping only takes about 5% of the total time when setting innerHTML. Another interesting observation is that Firefox is slower than MSIE in parsing that inner HTML. One star to MSIE here, to compensate for being 20 times slower above.
If we skip all DOM manipulation and zoom in to the purely logical part - the loops - we get:

The interesting aspect is that Prototype's each method is about 2 to 3 times slower than a raw for loop.
Note 1: I ran the tests on a second refresh of the page and with no other tabs or windows open.
Note 2: I got that annoying warning that "a script seems to run for a long time, and you do not seem to know what you are doing, let me take care of you" from Mozilla, but quickly clicked continue. Hope this does not effect the timings too much
PLEASE: if anybody knows how to turn off that over-protective warning, you know were to find me!
Side Note: when running these tests, and going from 1,000 to 10,000 div elements, it became clear how much larger 1 minute feels than 6 seconds. The cognito-emotional ratio is much higher than 10.
There will be more benchmarks for AJAX applications in future posts. And even comparisons to haXe abstract machine, neko. And perhaps even Flash.
Ok, back to my promise of providing a much more efficient element-finder, in cases with a lot of "uninteresting" elements in the DOM, getElementsByClassAndTag:
-
document.getElementsByClassAndTag =
-
function(className, tagName, parentElement) {
-
var children = ($(parentElement) ||
-
document.body).getElementsByTagName(tagName);
-
return $A(children).inject([], function(elements, child) {
-
if (child.className.match(new RegExp("(^|\\s)" +
-
className + "(\\s|$)")))
-
elements.push(Element.extend(child));
-
return elements;
-
});
-
}
The difference is actually big enough to warrant pushing the Prototype authors to include this function.
AJAX C++ cpsh firefox haxe ie javascript Language Reviews performance prototype script