I’ve been working on an interesting defect over the past few days around general slowness and “Slow Script” errors for webkit-based browsers (Safari and Chrome).  This is an issue that has popped up a few times over the last couple months, and we had yet to find the silver bullet.

In our previous attempts, our research (When innerHTML isn’t Fast Enough among others) had pointed us to the fact that setting innerHTML on a node in the DOM can be slow.  As a work-around we adopted a solution like this:

RALLY.setInnerHTML = function(el, html) {
if (RALLY.isIE || RALLY.isGecko) {
el.innerHTML = html;
return;
}

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

As you can see, when running in Safari or Chrome, remove the element from the DOM, then set its innerHTML, and finally put it back in the DOM.  With this fix in place, and by replacing occurrences of  el.innerHTML = someHtml with RALLY.setInnerHTML(el, someHtml), we were able to realize a marginal performance improvement.

Recently, however, we’ve been experiencing an increasing number of cases where Safari and Chrome are too slow, to the point where you see the spinning beach ball of death far too often.  The slowness always seems to be around operations that re-render large parts of the view.  As I started digging in and profiling operations, I was able to pinpoint the slowness to the insertBefore() and appendChild() DOM methods.  Looking at our RALLY.setInnerHTML method above, it’s easy to see where this happens.

But these are browser-implemented DOM methods.  How am I supposed to speed these up?  After banging my head on the desk for awhile, I sat down to rehash the problem with James (@james_estes) and Will (@trrbocharged).  They seemed to believe it had something to do with the content we were putting into the node outside of the DOM that was causing the insert/append to be so slow.  It turns out that in most cases, we were slamming a huge amount of content into the element while outside the DOM.  There was something about either the shape or content of this element that was causing the insert/append to be so slow.

After trying a few different things, we finally narrowed in on the styling and CSS classes.  We removed all the stylesheets, and ran through the benchmark tests again.  Things were amazingly fast. It seems that webkit is rather unbashful in triggering reflows when inserting/appending a node into the DOM that contains a lot of content.  So then we tried this:

RALLY.setInnerHTML = function(el, html) {
if (RALLY.isIE || RALLY.isGecko) {
el.innerHTML = html;
return;
}

var nextSibling = el.nextSibling;
var parent = el.parentNode;
var display = el.style.display || 'block';

el.style.display = 'none';
parent.removeChild(el);
el.innerHTML = html;
if (nextSibling) {
parent.insertBefore(el, nextSibling);
} else {
parent.appendChild(el);
}
el.style.display = display;
}

it was nearly as fast as the tests without the stylesheets.  For the given benchmark, we were able to reduce the time spent in the insertBefore() or appendChild() methods from 39 seconds down to 3 seconds.  With other data sets, we were able to realize up to a 30x improvement.

I’m continuing to research the root cause of this, but I’m guessing it has to do with how webkit processes the inserted/appended node and all of its children.  My initial throught is that as it builds the DOM and encounters classes and inline styles, it causes multiple reflows.  Perhaps the act of appending the display=none node supresses the need to trigger a reflow while building up the DOM, and the subsequent unsetting of display=none causes one (or a minimal set) of reflows to paint the new elements.