“It’s a fine line between pleasure and pain”, so sang the Divinyls. I can’t quite remember what the song was about, but they were probably singing about Ajax.

A plethora of libraries in the last year has meant it’s never been easier to take in the Ajax plunge, and we decided to take on YUI and yui-ext as we revamped the dashboard for Bamboo.

At first it was all pleasure. Within a day, we had the the status widget dynamically loading. Within two, there was Ajax tabs to the dashboard. Within two weeks, we had a brand spanking new dashboard and out went Bamboo RC1. Bamboo was looking sleek and sexy, and YUI-Ext was truly a joy to use (I’ll blog about some of the coolness later).

But then the pain started.

Firefox started chewing up memory and hunted down every spare ounce of CPU. After some initial it-works-on-my-box / firebug-leaks-memory denials, we did some tests.

Firefox Memory Usage IE Memory Usage Time
43Mb 40Mb 0:00
78Mb 227Mb 0:17

Houston, we have a problem.

Javascript Leaks In Action

Memory leaks are a pain at the best of times, but in-page client side leaks really aren’t fun. Our dashboard contain “portlets” which has links which updates the individual portlets, which also refreshes itself after a certain interval. The yui-ext libraries are pretty heavily used. Each div’s UpdateManger has its loadScripts flag enabled, in order to execute any JS that is contained within the loaded div.

Below are some of humps we stumbled into.

Leak 1: Closures

Essentially, a closure is an anonymous function that is declared inside a particular context, and thus has a reference to its parent context.
(or a more complete history)

The code below, which is run everytime a portlet is loaded, creates a closure.

function rewriteLinks(el, oResponseObject)
{
var updater = el.getUpdateManager();
var links = el.getChildrenByClassName('internalLink', 'a');
for( var i = 0; i < links.length; i++ )
{
links[i].addManagedListener('click', function(e) {
updater.update(this.dom.href);
});
}
}

The onClick handler keeps a reference to the anchor, and vice versa. The inner function is closed over the containing scope, and garbage collectors doesn’t clean it up. Every time we call rewriteLinks a new leaky reference is formed. (updater might also be leaked in the above example, but it should always be the same reference anyway )

The fix is to rewrite the function as a global function, better still a function that only needs to be attached once on the portlet div.

function ajaxClickHandlerForDiv(e, updateDivId)
{
var updater = getEl(updateDivId).getUpdateManager();
var link = e.findTarget('internalLink', 'a');
if (link != null)
{
e.preventDefault();
updater.update(link.href, null, null, true);
}
}

Interestingly, while there’s plenty of literature on the effect of closures across page reloads, especially on IE, there’s not a lot in terms of the effect on Ajax-style in-page reloads.

Leak 2: Uncleaned Events

Removing the closures did remove the leak on the test page, but the dashboard was still leaky, albeit at a reduced rate. Further investigations pointed suspiciously to:

<script type="text/javascript">
YAHOO.ext.EventManager.addListener("${toggleGroup_id}_toggler_on", "click", toggleOff, '${toggleGroup_id}');
YAHOO.ext.EventManager.addListener("${toggleGroup_id}_toggler_off", "click", toggleOn, '${toggleGroup_id}');
</script>

Was attaching an event to a DOM object causing it to be leaked?

Similar problems involves removing all event handlers on the window’s unload event. There’s no unload event on a div, but luckily, YUI-Ext comes to the rescue here with a beforeUpdate event, which fires before an update is made after an Ajax call. One solution would be to register all DOM objects that has an event handler. However, storing references to DOM objects inside Javascript objects is generally not advisable. Instead, we marked any object with an event handler with an “evented” class.

function clearHandlers(el)
{
var evented = el.getChildrenByClassName('evented');
for (var i = 0; i < evented.length; i++)
{
evented[i].removeAllListeners();
}
return true;
}

The clear handler method is then executed before each new Ajax update, clearing away all listeners.

Leak 3: Tips for Tools

With all events cleared and all closures removed, you’d think that’d be the end of it. Sadly, it wasn’t to be.

The third and somewhat obscure leak was the our usage of the YUI tooltip implementation. The somewhat innocuous looking code:

<div id="tooltipContent">
Some <strong>tooltip</strong> HTML
</div>
new YAHOO.widget.Tooltip("tooltipContent", { context:"targetDiv"})

had slipped quietly under the radar. We used the YUI tooltip by passing it a reference to a div which contained some HTML, which was then displayed a HTML styled popup. On further investigations, it’d appear that the framework would create a new div for each tooltip for each page refresh and attaches them to the <body> element. If you had 5 tooltips on your page, reloaded every 30 seconds or so for a few hours, this can really add up. Your DOM just gets bigger and bigger and bigger. Of course, being part of the <body> element means that it never gets cleaned up, and just stays there sucking up memory and CPU cycles

Luckily, tweaking the way we use the Tooltip widget to simply use the ‘text’ parameter managed to fix this issue (the divs attached to the body tag gets replaced, rather appended to). Unluckily, Internet Explorer seems to continue to use extra memory. In the end, we decided that this was about as far as we wanted to go and removed the tooltips for the RC2 release.

The Final Countdown

The law of leaky abstractions means that using libraries is a great starting place, but you still need to know how it all works; you’re going to have to get your hands dirty at some stage.

Testing for in-page leaks is tricky. I didn’t really find any tools that help with memory debugging, although FireBug is awesome for pretty much everything else. Moreover, it’s not really “leaking” in the traditional sense, which refers to memory that is retained across page loads. Is it just the browser not bothering to cleanup memory because it’s not gotten to a critical stage? Indeed, in IE, a page reload almost generally reclaims leaked memory. In an Ajax application which is expected to be kept opened for hours on end, in-page leaks are genuine issues.

The usability boost you get with prudent Ajax usage is considerable, but they come with their own set of challenges and concerns that’s not always as pleasurable as the end results themselves.