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.
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.
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:
Before | After | Net Win | |
---|---|---|---|
Project permapage | 443 ms | 211 ms | 2x |
To-do lists index | 555 ms | 93 ms | 6x |
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.
Zach Leatherman
on 14 Mar 12Can you go into more detail on why the sortables wouldn’t be initialized on touch screen devices?
It’s my understanding that most touchscreen browsers still fire the normal mouse events: mouseup, mousedown, click, etc. I did test this on my iPhone using ppk’s test page: quirksmode.org/js/events_mouse.html
If you’re referring to mouseover and mouseout, that’d make more sense to me.
Brandon
on 14 Mar 12Troll IE. Brilliant.
ErneX
on 14 Mar 12I don’t know if we are doing something wrong here, but we can change the order of the to-dos, but as soon as you refresh the page they go back to their previous order, so we are naming the to-dos with underscores at the start to have them in the order we want…
SS
on 14 Mar 12ErneX, please get in touch with support and we’ll be happy to help you out.
joe larson
on 14 Mar 12Love to see more technical articles on SVN. Good work…
Randy
on 14 Mar 12The interesting subtext is here is that optimization happened at the correct time in the development process; after a slowdown was noticed and more importantly, after the “slow” code was profiled.
I’m definitely guilty as charged when it comes to pre-optimization of code on a “gut feeling.”
James Brown
on 14 Mar 12Can you go into what tools you use for ‘Profiling the JavaScript’? Thanks!
Tom
on 14 Mar 12An interesting approach but a couple of things:
After the initialisation of a particular to-do list you should then remove it’s event listener, otherwise it’s needlessly firing the event each time.
Also rather than looking for ‘closest’ each time you should consider caching each to-do list in an object with the ID as the key, then inspecting the ID of the target of the click event then you can simply use the reference in your object. Searching the DOM = slow.
Tom
on 14 Mar 12I should add that point 1 would need you to attach event listeners to each to-do list rather than the whole document – much better anyway.
JD
on 14 Mar 12Awesome Sam.
JM
on 14 Mar 12Sam is awesome.
Ahmad
on 14 Mar 12Hey, this may not be relevant to your topic. But, do you have customers who use older versions of IE? I don’t think anyone sane who uses IE. And if so, do you have an idea why do they? convince them to change.
ErneX
on 14 Mar 12@SS opened the console and there’s this error once I move items on a to-do list:
TypeError: ‘undefined’ is not an object (evaluating ‘g0’)
ErneX
on 14 Mar 12Opened a ticket, it’s #139924 thanks.
Zack Grossbart
on 14 Mar 12I love the ideas here and speed is always great, but couldn’t you make this simpler with a timer? Can you let the page load and add a timer which waits a second or two before calling .sortable? That would make the page load fast and bind the sortables before anyone tries to sort anything.
You could also do more specific handling like waiting until the user moves the mouse over the sortable section.
Just a thought. I haven’t tried it. Thanks for the article.
-ZackAdam Hallett
on 14 Mar 12Did you use a library to do the benchmarks? What is the best practice on this?
Matt B
on 14 Mar 12Zack’s idea is the first one I had as well – seems like if you fire the sortable call 1000 or 2000ms after the page has loaded, you can avoid any sort of perceived overhead/drag between the mousedown event and the sortable call being complete.
Richard Nyström
on 14 Mar 12Good post! I love how much you care about making the performance better.
AntonioCS
on 14 Mar 12Nice work.
I’m with James Brown, share some light on what tools you used to profile please :)
Will Robertson
on 14 Mar 12I too like James Brown mentioned in the comments am interested in what profiling tools you use. Please share, thanks!
Yonathan Randolph
on 14 Mar 12IE was actually the first browser to support capturing mouse events (Netscape only supported bubbling), although you had to call Element.setCapture(true).
Allan Ebdrup
on 14 Mar 12If you are just adding behaviour in your onload, does it. Really matter. The user will not notice the thing running right?. Was the javascript running for half a second without pause? You could perhaps split it up into smaller pieces with setTimeout, that way you would avoid all the event stuff, and avoid delaying till mousedown, feeing you to do something else on mousedown. Why delay if you have a free processor right after load?
Sean McCambridge
on 14 Mar 12Along the lines of Matt B’s thought, couldn’t you just initialize those sortables last or in a callback after your last important piece of JS has run? It seems like the simplest solution would just defer init until everything else is in place—rather than having the very action you need initialized be the trigger for that initialization.
Not to pile on the way some people have in the wake of your new Basecamp launch, but it seems like some of these concepts/decisions defy the principles of 37s that I’ve been reading about for years. I mean, why say screw anyone using < IE9 when you could just find a simpler way to achieve the same thing?
radex
on 14 Mar 12@Sean, they say screw anyone using IE<9 anyway ;)
Ryan
on 14 Mar 12@Allan Ebdrup
They’ve already finished it and given stats on the speedup. What are you trying to prove?
My way would have been better. Hey everyone, look at me, I’m smart too. Bla, bla, bla.
redsquare
on 14 Mar 12All the major browsers have a js profiler built in (even ie)
In firebug and chrome you can use console.profile() and console.profileEnd() http://getfirebug.com/wiki/index.php/Command_Line_API#profile.28.5Btitle.5D.29
Joe
on 15 Mar 12I always treated javascript as not counted in perceived page load time. Unless it’s holding up presentation of a visible element, there is no visual indicator or other piece of feedback to a user that the page is still “loading”.
Tom
on 15 Mar 12Could you fix your CSS for the code tag? All the instances of “mousedown” were unreadable on my phone without pinching. It was a great article and I read it anyway, but it was tedious.
ErneX
on 15 Mar 12Test
Deeptechtons
on 15 Mar 12jst a teensy weensy doubt doesn’t the below code make the sortable’s sortable each time the sortStart event is invoked?
$(document).on(“sortstart”, ”.sortable_container”, function(event) { $(event.currentTarget).find(”> .sortable”).sortable(); });
Nic
on 15 Mar 12@Tom – It’s tiny like that in Chrome 17, FF10, and IE9 on Windows too. The smaller screen of a mobile device probably just exacerbates the issue.
Wouter
on 15 Mar 12Don’t know if this has been said yet, but class selectors are slooow (unless the browser has the non-standard getElementByClass method). The preferred way of using jQuery is providing the tag name followed by the class:
$(’.sortable’) vs $(‘li.sortable’)
See http://jqfundamentals.com/#chapter-9, “Optimize selectors”
SS
on 15 Mar 12The page load time concern described in this post doesn’t have to do with rendering time, or the time it takes to see the page on-screen. That’s largely a factor of server-side processing time and network speed. The time we’re measuring here is the time it takes to run all the callbacks that set up JavaScript behavior every time the page changes—in other words, how long it takes until you can begin interacting with the page in front of you.
If you try to use a page while it’s running these callbacks, scrolling will be slow or choppy, buttons won’t respond to clicks, and hovering won’t reveal the controls you expect. JavaScript is single-threaded, so other events on the page won’t be handled while page load callbacks are running.
That’s why waiting to initialize sortables a few seconds after the page loads isn’t a good solution. For one thing, it increases the effective time before to-dos on the page are ready for reordering. Perhaps more importantly, it would slow the page down during those first few moments where you’re likely to be scrolling or clicking on something else. And most of the time we’d be initializing a behavior that isn’t even used.
Minimizing the amount of initialization needed on each page load is the best path to a fast HTML UI. The technique in this post—deferring initialization until the point of interaction—is one way to achieve it. As always, you should measure, test, and choose the most appropriate technique for your application.
SS
on 15 Mar 12Regarding profiling:
For high-level measurements I use good old
new Date().getTime()
. One call before, one call after, subtract the former from the latter and log it to the console. (You can also use WebKit’sconsole.time()
andconsole.timeEnd()
functions.)When I want to dig deeper, I go to the WebKit Inspector’s Profile tab. You can turn it on or off through the UI, or use
console.profile()
andconsole.profileEnd()
in your code to control it programmatically.SS
on 15 Mar 12Zach Leatherman: in my testing, the mousedown event didn’t bubble up to the document on iOS. Perhaps there’s something else at play.
Deeptechtons: calling
sortable()
on an element that’s already sortable is effectively a no-op. So the initialization only happens once.Wouter: I used class names in this post to make for a simpler example. We actually use
[data-behavior]
selectors exclusively in our JS at 37signals.Either way, selector performance isn’t an issue here. All of our supported browsers support
querySelectorAll()
, and sortable initialization dwarfs selector time by two orders of magnitude. (That’s the kind of insight you’ll get from profiling.)Joe
on 15 Mar 12Sam: Thanks for the followup comments. Makes sense and good work.
Gabriel Mazetto
on 15 Mar 12@SS What about using Web Workers? (https://developer.mozilla.org/En/Using_web_workers)
SS
on 16 Mar 12Gabriel, web worker processes communicate over a message port and don’t have access to objects in the host environment. There’d be no way to initialize a sortable element using web workers.
rvagg
on 16 Mar 12SS, thanks for the post, nice to see this level of nerdery here! However, it seems that the main win for you with capturing is simply that you can’t hack .sortable()to respond to the current event as it’s happening so you get to take a step back in the event process. I think my response to that would be that this points to a deficiency in the implementation of .sortable() rather than the need for JS libs to start taking capturing more seriously.
Imagine a world in which capturing was easier and more widely used and .sortable() used it for its own internal purposes. Then if you wanted to apply your optimisation to it you’d need to take another step back in the event chain but there isn’t one.
Your comments made me ponder if this is something we could add to Bean in some way but I’m not sure I’m convinced of the utility.
Allan Ebdrup
on 16 Mar 12@Ryan Trying to prove? NOthing, I’m just interested in the best solution. It would seem that if are measuring the wrong thing, you might be better off not measuring. I mean if you’re measuring load times, and including things that the user doesn’t experience in the measurement, who cares if you can improve the measurement? But now you have an overly complex solution for a non-problem. I’m not saying, that’s what happened here. That’s why I formed my comment as questions, I genuenly don’t understand why this complex solution should be nessesary. But perhaps there is a good reason.
Jake
on 16 Mar 12Will definitely improve my performance sureshot ! Thanks man.
ADIpump
on 16 Mar 12Oh wow, I didn’t know you could even do this. Thanks Jake.
Kevin
on 16 Mar 12This is a cool approach, but leaves me wondering: If adding sortable takes 200-450 milliseconds (based on the numbers you reported), does this create a user-visible “lag” when they first try to do a drag & drop? That initialization cost has to happen somewhere…
Scott
on 16 Mar 12I relent. At first I thought all this work around Basecamp performance was not really the best use of time nor talent. But now that I use Basecamp alongside other applications, the other apps suddenly seem agonizingly slow.
Way to ruin the experience of other apps for me guys! :)
Dave Furfero
on 16 Mar 12“Always put off until tomorrow what you don’t absolutely have to do today.” – Jefferson Thomas
Deferred initialization is a great optimization technique. Nice to see a good writeup with some advanced tweaks. I especially like the switch to capture to handle deferred init of items with similar events.
Also, I assume you’re using jQuery UI’s sortable? If so, enable touch events with one small script :) https://github.com/furf/jquery-ui-touch-punchZach Leatherman
on 16 Mar 12You’re right Sam, it looks like only certain elements trigger mousedown/mouseup events on mobile.
In this test case it fires mousedown/up/click on buttons, but not paragraphs or list items in iOS 5.1.
I will have to do additional testing to figure out which elements trigger mouse events and which do not.
Amber Feng
on 16 Mar 12Pretty cool. Had the exact same question as Kevin – if initializing sortable can take up to half a second on heavy pages, won’t the users see this lag when they attempt to click and drag a sortable element?
I’m imagining a scenario where a user isn’t really interacting with the page for a couple of seconds (free time to initialize!), perhaps reading to-do list items, and then attempts to drag a sortable. In the case that you had initialized on page load or even delayed the initialization with setTimeout, they wouldn’t be seeing the lag.
Interested to hear your thoughts on this.
Joel Plane
on 17 Mar 12@Zach I believe any element with style “cursor: pointer” will trigger a mousedown event. If you add that style to any element, it will gain the ability to trigger mousedown/up/click events. I could be mistaken, and I can’t test it now.
pomeh
on 17 Mar 12Great content, thanks ! :)
I you want to see more example like this one, you could see the presentation “Contextual jQuery in Practice” by Doug Neiner at http://speakerdeck.com/u/dougneiner/p/contextual-jquery-in-practice
He talks about one time initialization pattern, probable user actions, automatic initialization and predicting user actions.
Applying the presentation to this example, it could be possible to listen for the “mouseenter” event on a parent element, instead of the “mousedown”.
This:
- remove the problem about bubbling,
- works on every browser,
- as the event is raised far before the user’s dragging action, the browser as the time to do all initialization between the mouse enter and the real user’s drag action,
- the only payload is that it increase false positive (so the performance gain is a little smaller) because every mouseenter won’t end with a dragging action, but I think this could be considered as not relevant/significative when applying the “one time initialization pattern”
What do you think ?
Alec
on 17 Mar 12As Zach said, wouldn’t initializing sortables on mouseover be good enough?
Ruby on rails development
on 18 Mar 12I wonder why new applications would still support older browsers. Forcing positive change is not too bad i would say.
icaaq
on 19 Mar 12Nice idea, but what about people that only uses keybord for navigation? eg. AT-users?
Zach Leatherman
on 19 Mar 12@Joel Plane: You’re right, adding cursor: pointer made the mousedown/up, click events fire. Great info, thanks.
SS
on 19 Mar 12Initializing on mouseover would be an improvement over initializing on page load, but it’s still problematic: you’re far more likely to trigger a mouseover event when you aren’t intending to reorder to-dos. And you’ll probably trigger it while scrolling—another place where performance is a concern.
I hope I’ve shown in this post that using capturing to defer initialization to mousedown is better than initializing on mouseover, and just as easy.
SS
on 19 Mar 12Some of you have correctly pointed out that we still pay the price of initialization when it’s deferred.
This is where the second pass at optimization (in the “Taking it Further” section) helps. Instead of initializing all sortables on the page, we start by initializing only the sortable closest to the point of interaction. Then we progressively expand when sorting begins.
At some point the question of whether or not to continue optimizing comes down to feel: does reordering feel slow? In this case, the answer was no. If it had felt slow, I’d have run the profiler again and worked from there.
This discussion is closed.