Abstract: Rich web applications with large DOMs can create a new class of performance headaches for web developers. Seb Ruiz discusses this problem as recently faced in the user interface rewrite of FishEye and Crucible 2.0, his team’s solution and the pitfalls.

In a Bind

An increasingly common challenge for developers of rich web applications these days is the performance (or lack thereof) in large pages that have lots of complex JavaScript powered functionality. When Firefox freezes on you on some random web page, there’s a pretty good chance its caused by the very JavaScript designed to improve your experience.
Asynchronous loading and updating content via AJAX calls, error reporting, dynamic visualizations, real-time updates and complicated layouts. The FishEye and Crucible teams had to deal with all of these for our recent 2.0 release. However the biggest problem we faced wasn’t adding all this new functionality, but rather, it was making it performant in standard browsers.
This post will hopefully provide some tips on how to mitigate the effect of combining some complex JavaScript with a large html document structure. It’s generally regarded as good practice to minimise html document size as much as possible, but there are often situations where it is unavoidable. A large FishEye annotation page will have up to as many as 80,000 DOM elements in the page – this isn’t so surprising considering a single line of syntax source can easily contain more than 10 html elements once you take into account syntax highlighting and blame.
Even if you aren’t faced with stupidly large DOMs its still a good thing to understand what’s going on behind the magic veil of JQuery.

The event binder

A simple jQuery event bind selector might look like this:

$(document).ready(function () {
$(“.alert-on-click”).bind(“click”, function () {
alert(“Clicked element ” + this);
});
});

This is a rather standard method of binding functions to events with jQuery. It’s easy and it’s elegant. When the html document has finished loading, the anonymous function is executed. This will find all elements which have the class ‘alert-on-click’ in the document, and binds a function which is triggered on a click event.

Slow class selectors

This method can be problematic with large html documents with thousands of DOM elements. Web browsers which aren’t able to do efficient evaluations on class based selectors are seriously disadvantaged here as they need to trawl through the entire document tree to find the elements. Other browsers are better off, but it’s still a high cost
operation relative to the super fast id based selector.

The cost of the bind

For moment, imagine that you are Crucible as a web application. You’ve been asked to display a review which has 10,000 lines of code visible. Seems simple – throw each of the lines into a table for easy and nice rendering. Then you need to make sure that whenever the user clicks on lines of code, that they are able to create a comment on that requested line. Simple! Bind an a click event handler to the source lines.

$(document).ready(function () {
$(“tr.sourceLine”).bind(“click”, showCommentBoxFn);
});

However in this case the browser needs to find 10,000 tr elements, and make 10,000 bind calls. This is noticeably expensive and will slow down the load of the page. Furthermore, it’s easy to miss that for memory management reasons, jQuery also unbinds all bound elements on a click away from a page. That means a slow load, but also a slow unload.

How to count binds

Here’s an easy trick to find out just how many bind and calls you are making when loading your page. Insert this point cut into your JavaScript to add some debugging to jQuery bind events. You’ll need firebug installed and enabled to see the output.

jQuery.fn.bind = function (bind) {
return function () {
console.count(“jQuery bind count”);
console.log(“jQuery bind %o”, this);
return bind.apply(this, arguments);
};
}(jQuery.fn.bind);

Bind Count Annotation

Bind synchronisation with AJAX calls

There is a significant amount of interactive functionality in FishEye/Crucible. When new elements are inserted into the document, we must always ensure that they are bound appropriately. Buttons, links, interactive elements all need to be kept track of; and let’s face it – this can be a chore. A subtle change in the document structure can silently break functionality.

Moving away from traditional binding

The solution that FishEye and Crucible have taken to solving this problem is by using event delegation in the form of jQuery live events. In JavaScript, if an event isn’t caught by an element then it “bubbles” to it’s parent, and so forth. When using event delegation, a function will be bound for all current and future elements which match a particular jQuery selector.
Using a live event is just as simple as conventional binding. Simply substitute “bind” with “live”

$(document).ready(function () {
$(“tr.sourceLine”).live(“click”, showCommentBoxFn);
});

Using live events will also avoid a blocking interface caused by excessive binding in the page load and unload.
Bind vs Live Speeds

Live event pitfalls

We discovered a couple of unexpected problems when using live events.
Binding with live events still needs to evaluate the selector expression, which means that $(“.class”).live() can still be very slow if the DOM is large. We worked around this by loading the large chunks of data with an ajax call after initialising the page view and running our live event binding.
Using live events on mouse events (click, dblclick, mousepress etc) accepts events from the right mouse button when they normally aren’t desirable. For example, a right click on a link to copy a gracefully degrading target url would cause the bound event to fire. We solved this by reimplementing jQuery’s live function to ignore events caused by right clicks – see the source
Quasi race conditions due to the non deterministic execution order of live events. If an element matches more than one live event selector, then the order which in these event functions are executed is not guaranteed. For example:

$(document).ready(function () {
$(“div.comment”).live(“click”, function () {
markCommentAsRead();
});
$(“div.comment a.reply”).live(“click”, function () {
replyToComment();
// there is no way to prevent a propagation to div.comment
});
});

In this case, when the <a class=”reply”> link is clicked, both replyToComment() and markCommentAsRead() will be executed.

Fresh ideas, announcements, and inspiration for your team, delivered weekly.

Subscribe now

Fresh ideas, announcements, and inspiration for your team, delivered weekly.

Subscribe now