Thursday, September 13, 2007

replaceHTML - remove, insert, put back is better

** Updated **
Steven put up another speed demo that demonstrates that replaceHtml is in fact faster than the remove-update-replace method, though not by nearly the same gap as innerHTML.
*************

Read an piece on Ajaxian this morning covering Steven Levithan's replaceHTML post on innerHTML being too slow and offering a replaceHtml alternative that is much faster. It works by creating a new element, using innerHTML on it, then replacing the existing node with the new node. Here is the code:


/* This is much faster than using (el.innerHTML = value) when there are many
existing descendants, because in some browsers, innerHTML spends much longer
removing existing elements than it does creating new ones. */
function replaceHtml(el, html) {
var oldEl = (typeof el === "string" ? document.getElementById(el) : el);
/*@cc_on // Pure innerHTML is slightly faster in IE
oldEl.innerHTML = html;
return oldEl;
@*/
var newEl = oldEl.cloneNode(false);
newEl.innerHTML = html;
oldEl.parentNode.replaceChild(newEl, oldEl);
/* Since we just removed the old element from the DOM, return a reference
to the new element, which can be used to restore variable references. */
return newEl;
};


This is a lot faster, but there really isn't any need to create a new node. The reason the method is so much faster is due to using innerHTML on an element that is not in the DOM vs. one that is in the DOM. Instead of creating a new node and deleting the old one, you can just take the node out of the DOM, use innerHTML, and then put it back. This has the added benefit of not invalidating any existing references to your element.

I whipped up an example to compare the differences. I generated an HTML string with 15,000 elements and used three different methods for updating a a div to this content - innerHTML, removing the node before innerHTML, and then the replaceHtml function. I did each four times and profiled with Firebug. Here is the script:


var html = [];
for( var i = 0; i < 15000;i++ )
{
html.push("<span>CONTENT</span>");
}
html = html.join("");
var el = document.getElementById("div");
el.innerHTML = html;
function doit(){
withoutRemove(el, html);
withRemove(el,html);
el = replaceHtml(el, html);
withoutRemove(el, html);
withRemove(el,html);
el = replaceHtml(el, html);
withoutRemove(el, html);
withRemove(el,html);
el = replaceHtml(el, html);
withoutRemove(el, html);
withRemove(el,html);
el = replaceHtml(el, html);
}
function withoutRemove(el,html){
el.innerHTML = html;
}

function withRemove(el,html){
var nextSibling = el.nextSibling;
var parent = el.parentNode;
parent.removeChild(el);
el.innerHTML = html;
if(nextSibling)
parent.insertBefore(el,nextSibling)
else
parent.appendChild(el);
}

function replaceHtml(el, html) {
var oldEl = el;
/*@cc_on // Pure innerHTML is slightly faster in IE
oldEl.innerHTML = html;
return oldEl;
@*/
var newEl = oldEl.cloneNode(false);
newEl.innerHTML = html;
oldEl.parentNode.replaceChild(newEl, oldEl);
/* Since we just removed the old element from the DOM, return a reference
to the new element, which can be used to restore variable references. */
return newEl;
};


And here are the results:

Method Average Time
------------- ------------
withoutRemove 4914.063ms
replaceHtml 656.25ms
withRemove 601.563ms

1 comments:

said...

That doesn't seem to be enough for a true benchmark. You are only testing it in FF. If you compare techniques, you should try it on different environments.