We’ve been busy tightening the nuts and bolts on the all-new Basecamp in the wake of last week’s launch. As part of the process, I decided to take a closer look at client-side page load performance. Shaving even a tenth of a second off of page load time can have a big impact on perceived performance, so it’s a worthwhile investment.

Profiling the JavaScript page load event led to a surprising revelation. On pages with many to-dos, an overwhelming majority of the time was spent initializing sortable behavior. The culprit looked something like this:

$.ready(function() {
  $("article.todolist, section.todolists").sortable();
});

In other words, when you’d load any page with to-dos on it, Basecamp would make the items of each to-do list reorderable, then make the whole collection of to-do lists reorderable. That could take up to half a second on heavy pages.

Here’s the thing: you probably aren’t reordering to-dos every time you visit a project. It would be best if we could avoid initializing sortables until the last possible moment, just before you start to drag a to-do.

Deferring Initialization

That got me thinking. What would happen if we tried waiting until the first mouse press before initializing sortables? The mousedown event gets us a lot closer to the intention of reordering than the page load event. I tried it out:

$(document).on("mousedown", ".sortable_handle", function() {
  $("article.todolist, section.todolists").sortable();
});

The new code says that when we receive a mousedown event on the page that comes from somewhere inside a sortable drag handle, we should go ahead and initialize all the sortables on the page.

Sadly, this code doesn’t work. The sortable() initialization installs a mousedown handler of its own, but by that time it is too late. The event has already bubbled its way up to our delegated event handler.

If only there were a way to catch the mousedown event just before it begins its ascent up the DOM

Phases and Stages

W3C’s DOM Level 2 Events API specifies two phases for events: capturing and bubbling. The third argument to the addEventListener method lets you specify which phase you’re concerned with when you register to be notified of events on an element. How are these phases different?

Bubbling

Bubbling is the phase we use in everyday development. When an event like mousedown is triggered on an element, any event listeners on that element are notified. Then the listeners on each parent element are notified in succession until propagation is canceled or the event reaches the outermost element on the page.

Bubbling

When you use event delegation techniques like jQuery’s delegate method or Backbone view events, you’re taking advantage of the bubbling phase.

Capturing

The capturing phase is the inverse of the bubbling phase. Capturing happens before bubbling, and in reverse order—instead of starting on the target element and propagating outwards, the outermost element is notified of the event first. Then the event makes its way down the hierarchy until it reaches the target.

Capturing

The mechanics of the capturing phase make it ideal for preparing or preventing behavior that will later be applied by event delegation during the bubbling phase. And that’s how we’re going to use it here—to initialize the sortable in response to a mouse click, but just before the event starts bubbling and other handlers have a chance to deal with it.

Implementing Capturing

To make use of capturing, we have to go down to the metal. jQuery’s event methods only work for bubbling and don’t let us tap into the capturing phase. The capturing handler looks like:

document.addEventListener("mousedown", function(event) {
  if ($(event.target).closest(".sortable_handle").length) {
    $("article.todolist, section.todolists").sortable();
  }
}, true);

(Note the true at the end of the addEventListener call—that’s what tells the browser to use capturing.)

Now the expensive sortable initialization is deferred until the point of intent. Reordering works again and our page loads are snappy!

A nice consequence of this approach is that we’ll no longer waste time initializing sortables on touch devices, since they’ll never trigger mousedown events. (Eventually we’d like to get reordering working on all devices, but we’ll save that for a later iteration and take the performance win now.)

“But I Can’t Use It!”

Capturing works in browsers that support the W3C event model. That means all modern browsers from the last ten years, and Internet Explorer as of version 9.

We’re in a lucky place because the new Basecamp requires IE 9 or higher. But we could use this capturing technique even if we needed to support older versions of IE by conditionally falling back to the older code where necessary.

Don’t be blindsided by conventional wisdom. Before dismissing capturing because you can’t use it everywhere, consider the places you can use it to progressively enhance the experience for those with a more modern browser.

Taking It Further

Deferred initialization is a definite improvement, but we’re still doing more work on mousedown than we need to. Consider the case where you click on a to-do to check it off or visit its permapage. It’s unnecessary to initialize every sortable on the page at click time—we need only to worry about the sortable under the mouse pointer.

So let’s scope our capture handler down to just the current sortable:

document.addEventListener("mousedown", function(event) {
  $(event.target).closest(".sortable").sortable();
}, true);

This works for reordering to-dos within a single list, but now it’s not possible to move a to-do from one list to another. We can fix it by listening for a sortstart event and then initializing all the sortable’s siblings:

$(document).on("sortstart", ".sortable_container", function(event) {
  $(event.currentTarget).find("> .sortable").sortable();
});

Now we do as little initialization work as possible on mousedown and defer the heavy lifting until the first mouse movement after clicking, when we know you really want to reorder.

Measuring the Win

How does the deferred initialization approach pay out in practice? Here are the results from measuring the time it takes to execute all page load callbacks on a sample project, averaged over five runs:

BeforeAfterNet Win
Project permapage443 ms211 ms2x
To-do lists index555 ms93 ms6x


Not bad for just shuffling a few lines of code around!

If you’re looking to speed up page load times in your own projects, start by profiling. Then identify the hotspots and defer them until the furthest point of intent—where you know the user is about to perform the action you need to set up.