My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Wednesday, April 01, 2009

TaskSpeed: DOM VS Libraries

I tried to create an essential library to perform a manual implementation of the TaskSpeed test suite and results surprised me.

All these web 2.0 libraries are extremely useful, cool, time-saving, whatever you want, but not a single one can compete with manual tasks implementation, specially when we are talking about the most common browser which at the same time is the slowest ever: Internet Explorer!


Final Results for an Dual Core under Windows XP SP3


FireFox 3.0.8
------------------------
library | ms
------------------------
DOM 383
plugd-a (Dojo) 734
Dojo 1.3.0 742
Dojo 1.2.3 1008
MooTools 1.2.1 1252
jQuery 1.3.2 1935
jQuery 1.2.6 3518


Safari 4 Beta (528.16)
------------------------
library | ms
------------------------
DOM 118
plugd-a (Dojo) 170
Dojo 1.3.0 186
Dojo 1.2.3 342
MooTools 1.2.1 347
jQuery 1.3.2 602
jQuery 1.2.6 1025


Opera 10.00 alpha
------------------------
library | ms
------------------------
DOM 111
Dojo 1.3.0 251
plugd-a (Dojo) 374
MooTools 1.2.1 688
Dojo 1.2.3 938
jQuery 1.3.2 1329
jQuery 1.2.6 2327


Internet Explorer 6
------------------------
library | ms
------------------------
DOM 3966
Dojo 1.2.3 22750
plugd-a (Dojo) 24189
Dojo 1.3.0 24249
jQuery 1.3.2 44314
MooTools 1.2.1 69346
jQuery 1.2.6 104407


Internet Explorer 7
------------------------
library | ms
------------------------
DOM 612
Dojo 1.2.3 3641
plugd-a (Dojo) 3748
Dojo 1.3.0 3861
jQuery 1.3.2 5031
MooTools 1.2.1 7658
jQuery 1.2.6 10468


Internet Explorer 8
------------------------
library | ms
------------------------
DOM 499
Dojo 1.3.0 2327
plugd-a (Dojo) 2455
Dojo 1.2.3 2922
jQuery 1.3.2 3170
MooTools 1.2.1 5876
jQuery 1.2.6 7049



The essential library


var $ = {
// essential stuff for TaskSpeed test by WebReflection
// Mit Style License
addEventListener:document.addEventListener?
function(name, callback, bool){
this.addEventListener(name, callback, bool);
}:
function(name, callback, bool){
this.attachEvent("on" + name, callback);
}
,
getSimple:function(selector){
for(var
split = selector.split("."),
result = [],
re = new RegExp("\\b" + split[1] + "\\b"),
list = this.getElementsByTagName(split[0] || "*"),
length = list.length,
i = 0,
j = 0,
node;
i < length; ++i
){
node = list[i];
if(re.test(node.className))
result[j++] = node;
};
return result;
},
indexOf:Array.prototype.indexOf || function(value, i){
for(var
length = this.length,
i = i < 0 ? i + length < 0 ? 0 : i + length : i || 0;
i < length && this[i] !== value;
++i
);
return length <= i ? -1 : i;
},
removeEventListener:document.removeEventListener?
function(name, callback, bool){
this.removeEventListener(name, callback, bool);
}:
function(name, callback, bool){
this.detachEvent("on" + name, callback);
}
,
slice:Array.prototype.slice
};


The test suite

// TaskSpeed, the DOM way by WebReflection
window.tests = {

"make": function(){
for(var
body = document.body,
ul = document.createElement("ul"),
li = document.createElement("li"),
i = 0,
fromcode;
i < 250; ++i
){
fromcode = ul.cloneNode(true);
fromcode.id = "setid" + i;
fromcode.className = "fromcode";
fromcode.appendChild(li.cloneNode(true)).appendChild(document.createTextNode("one"));
fromcode.appendChild(li.cloneNode(true)).appendChild(document.createTextNode("two"));
fromcode.appendChild(li.cloneNode(true)).appendChild(document.createTextNode("three"));
body.appendChild(fromcode);
};
return $.getSimple.call(body, "ul.fromcode").length;
},

"indexof" : function(){
for(var body = document.body, index = -1, i = 0; i < 20; ++i)
index = $.indexOf.call(body.getElementsByTagName("ul"), document.getElementById("setid150"));
return index;
},

"bind" : function(){
for(var callback = function(){}, li = document.body.getElementsByTagName("li"), length = li.length, i = 0, total = 0, node; i < length; ++i){
node = li[i];
if(node.parentNode.nodeName == "UL"){
++total;
$.addEventListener.call(node, "click", callback, false);
};
};
return total;
},

"attr" : function(){
for(var result = [], ul = document.body.getElementsByTagName("ul"), length = ul.length, i = 0; i < length; ++i)
result[i] = ul[i].id;
return result.length;
},

"bindattr" : function(){
for(var callback = function(){}, li = document.body.getElementsByTagName("li"), length = li.length, i = 0, total = 0, node; i < length; ++i){
node = li[i];
if(node.parentNode.nodeName == "UL"){
++total;
$.addEventListener.call(node, "mouseover", callback, false);
node.setAttribute("rel", "touched");
$.removeEventListener.call(node, "mouseover", callback, false);
};
};
return total;
},

"table": function(){
for(var
body = document.body,
table = document.createElement("table").appendChild(document.createElement("tbody")).parentNode,
tr = document.createElement("tr"),
td = document.createElement("td"),
length = 40,
i = 0,
madetable,
cell;
i < length; ++i
){
madetable = table.cloneNode(true);
madetable.className = "madetable";
cell = body.appendChild(madetable).firstChild.appendChild(tr.cloneNode(true)).appendChild(td.cloneNode(true));
cell.appendChild(document.createTextNode("first"));
cell.parentNode.insertBefore(td.cloneNode(true), cell);
};
tr = body.getElementsByTagName("tr");
length = tr.length;
i = 0;
for(var total = 0; i < length; ++i)
total += tr[i].getElementsByTagName("td").length;
return total;
},

"addanchor" : function(){
var a = document.createElement("a");
a.setAttribute("href", "http://example.com");
a.appendChild(document.createTextNode("link"));
for(var ul = $.getSimple.call(document.body, "ul.fromcode"), length = ul.length, i = 0, total = 0, childNodes, j, len, node; i < length; ++i){
childNodes = ul[i].childNodes;
j = 0;
len = childNodes.length;
while(j < len){
node = childNodes[j];
if(node.nodeName === "LI"){
++total;
node.appendChild(a.cloneNode(true));
};
++j;
};
};
return total;
},

"append": function(){
for(var div = document.createElement("div"), body = document.body, i = 0, node; i < 500; ++i){
node = div.cloneNode(true);
node.setAttribute("rel", "foo2");
body.appendChild(node);
};
for(var div = body.getElementsByTagName("div"), length = div.length, i = 0, total = 0; i < length; ++i)
total += div[i].getAttribute("rel") === "foo2";
return total;
},

"addclass-odd" : function(){
for(var div = document.body.getElementsByTagName("div"), length = div.length, i = 0, total = 0; i < length; ++i)
total += i % 2 ? !!(div[i].className += " added odd") : !(div[i].className += " added");
return total;
},

"style" : function(){
for(var div = $.getSimple.call(document.body, "div.added"), length = div.length, i = 0, style; i < length; ++i){
style = div[i].style;
style.backgroundColor = "#ededed";
style.color = "#fff";
};
return length;
},

"removeclass" : function(){
for(var re = /\s*\badded\b/g, div = $.getSimple.call(document.body, "div.added"), length = div.length, i = 0, node; i < length; ++i){
node = div[i];
node.className = node.className.replace(re, "");
};
return length;
},

"sethtml": function(){
for(var div = document.body.getElementsByTagName("div"), length = div.length, i = 0; i < length; ++i)
div.innerHTML = "<p>new content</p>";
return div.length;
},

"insertbefore" : function(){
for(var p = document.createElement("p"), ul = $.getSimple.call(document.body, "ul.fromcode"), length = ul.length, i = 0, total = 0; i < length; ++i){
for(var a = ul[i].getElementsByTagName("a"), len = a.length, j = 0, node; j < len; ++j){
++total;
node = a[j];
node.parentNode.insertBefore(p.cloneNode(true).appendChild(document.createTextNode("A Link")).parentNode, node);
};
};
return total;
},

"insertafter" : function(){
for(var p = document.createElement("p"), ul = $.getSimple.call(document.body, "ul.fromcode"), length = ul.length, i = 0, total = 0; i < length; ++i){
for(var a = ul[i].getElementsByTagName("a"), len = a.length, j = 0, node; j < len; ++j){
++total;
node = a[j];
node.parentNode.appendChild(p.cloneNode(true).appendChild(document.createTextNode("After Link")).parentNode);
};
};
return total;
},

"destroy": function(){
var result = $.getSimple.call(document.body, "ul.fromcode"),
length = result.length,
i = 0,
node;
while(i < length){
node = result[i++];
node.parentNode.removeChild(node);
};
return length;
},

"finale": function(){
var body = document.body;
while(body.firstChild)
body.removeChild(body.firstChild);
return body.getElementsByTagName("*").length;
}

};


Conclusion

I do not want to spend a word about these results, but I guess we have to think about them.

6 comments:

Àl said...

What happened to jQuery? jeje. I remember all that attacks to MooTools with respect to Sizzle.

Anonymous said...

I think you've totally missed the point of this test.
You've prepared a code that addresses this test individually, code which would be hardly usable in real world application and you try to compare it to widely used toolkits.

If you think that cloneNode is more efficient than createElement and can be used in place of it whatever the case is.. then you should declare your own universal createElement in your '$' object and call that within the test.
Iterations within tests are also made to get more comparable time results and not to test adding 250 elements at once (as it's rather not real world case) - you however are creating DOM tree and then clone that within 'for' statement - how it measures single create element case ?
One of the test says 'find all the first children li's of all nodes with class="fromcode"' - this is clearly about ".fromcode" and not "ul.fromcode" (which everyone knows is more efficient) and in your test you're using "ul.fromcode", just because you know that in that specific document no other element have that class (?)

If you want to show better solutions than those in tested libraries,
You should present your own implementation of 'querySelector' method, your own library that deals nice with memory leaks in IE and other known issues and then put that very generic solution to this test.. it would be fair then :)

Andrea Giammarchi said...

Medyk, all tasks are optimized and computed manually on purpose and I am not missing a single point.

If you know the DOM you are working on, there is no reason to search for .fromcode rather than ul.fromcode, since the task before asks me to create them.

This post is not about generic DOM performances or TaskSpeed scalability, this post aims is to show how tremendously bad Internet Explorer is performing against every other browser and in the worse case scenario it is about 3 seconds against 100.

Dojo team, as I wrote in ajaxian as well, simply considered the good welcome to compare worst bottleneck but I guess everybody that did not get what this post is about will write something: "obviously DOM and manual tasks are fasters" or stuff like "ASM vs Python" ... pointless, cause here we have a different problem, IE performances!

Unknown said...

Nice work Andrea.

Anonymous said...

Andrea,
Then you should alter other libraries test code so it has same logic as yours - then it would be comparable.

Anyway I'm neither fan of those tested popular tools in in my work I rely as close as it is reasonable on native browsers engine. I know that I sounded this way but after all I don't want to defend their performance.

David Mark said...

"You've prepared a code that addresses this test individually, code which would be hardly usable in real world application and you try to compare it to widely used toolkits."

That's how you write real code for the "real world." Using jQuery, MooTools or other nonsensical scripts is for the fantasy world (as this test confirms.)

Speed is the least of the problems anyway. These compilations of observed browser quirks masquerading as cross-browser scripts are inconsistent and incompatible with anything but a handful of modern browsers in their default configurations.

Relying on others to solve every problem known to browser scripting is a long and winding dead-end. How many problems do you need to solve for your current app's design?