Wednesday, November 4th, 2009

jQuery Bondage

Category: jQuery, Performance

<blockquote>

When your browser 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.

Good ‘ole JavaScript performance. Sebastian Ruiz of Atlassian recently worked on a UI rewrite of two of their products (FishEye and Crucible) and found some interesting solutions to problems that came up:

The event binder

A simple jQuery event bind selector might look like this:

javascript
< view plain text >
  1. $(document).ready(function () {
  2.          $(".alert-on-click").bind("click", function () {
  3.               alert("Clicked element " + this);
  4.           });
  5.      });

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.

javascript
< view plain text >
  1. $(document).ready(function () {
  2.          $("tr.sourceLine").bind("click", showCommentBoxFn);
  3.      });

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.

javascript
< view plain text >
  1. jQuery.fn.bind = function (bind) {
  2.      return function () {
  3.           console.count("jQuery bind count");
  4.           console.log("jQuery bind %o", this);
  5.           return bind.apply(this, arguments);
  6.       };
  7. }(jQuery.fn.bind);

Bind Count Annotation

The team moved away to the not-so-recently jQuery addition, live queries:

javascript
< view plain text >
  1. $(document).ready(function () {
  2.          $("tr.sourceLine").live("click", showCommentBoxFn);
  3.      });

and found a few pitfalls:

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:

javascript
< view plain text >
  1. $(document).ready(function () {
  2.          $("div.comment").live("click", function () {
  3.               markCommentAsRead();
  4.           });
  5.  
  6.          $("div.comment a.reply").live("click", function () {
  7.               replyToComment();
  8.               // there is no way to prevent a propagation to div.comment
  9.           });
  10.      });

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

Posted by Dion Almaer at 6:16 am
44 Comments

+++--
3.1 rating from 111 votes

44 Comments »

Comments feed TrackBack URI

event delegation anyone? that should solve 10 000 row long table problems as there is only one event handler and it is attached to the table not individual rows. that also solves dynamically refreshed table contents.

Comment by jx12345 — November 4, 2009

The link to the original article is missing.

This is something that happens a lot with AJAXian articles. Maybe I’m missing something obvious.

Comment by melo — November 4, 2009

2nd jx12345
Use event delegation where it can be used.
Though kinda suprised live() appearantly does not delegate events that can be delegated as this article suggests…

Comment by BenGerrissen — November 4, 2009

Event delegation will resolve a lot these issues, and can make your code loads easier to use.

All JavaScript developers should familiarize themselves with these techniques and understand when to use which technique.

http://icant.co.uk/sandbox/eventdelegation/

http://www.danwebb.net/2008/2/8/event-delegation-made-easy-in-jquery

http://robertnyman.com/2008/05/04/event-delegation-with-javascript/

Comment by MorganRoderick — November 4, 2009

And supplying code that ignores rightclick event from a framework? ajaxian!
e.which + event delegation ftw.

Comment by BenGerrissen — November 4, 2009

I fully agree with jx12345.

This is really, really, really not the way to do it. Write one function that findes out what the originating element is, and then bind that handler to the table – not each row!

This can be done in a very simple and cross browser manner.

Comment by MichaelSchoeler — November 4, 2009

Event delegation is key here, I don’t really use jQuery and have never heard about live() and what it does but surely setting an id on the table and observing clicks on that and checking it was a TR would be much better?

Comment by Phunky — November 4, 2009

There’s a lot of writups out there with crappy event delegation techniques. jquery doesn’t have Prototype’s event.findElement but you can easily write your own using .closest and event.target, jquery could have it with 5 extra lines of code. Probably the most elegant way to handle event delegation: http://mislav.uniqpath.com/js/handling-events-on-elements/

Comment by Jadet — November 4, 2009

And by the way, if you’re worried about performances of live() with class selectors, you should check out this post: http://www.lrbabe.com/?p=530
and use an alternative implementation of the jQuery.filter() method available as a gist: http://gist.github.com/169693

Comment by frenchStudent — November 4, 2009

> 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.

Here’s how to do lazy evaluation of a live selector:
var jqElem = $(document);
jqElem.selector = ‘li.ui’;
jqElem.live(‘dblclick’, dblhandler);

This removes the need to do an extra selection, however it will still be tested against each element as the events bubble up. The key here is to use this technique in combination with a very specific and fast selector.

More about both in my jQuery Anti-Pattern for Performance slides. :)

Comment by PaulIrish — November 4, 2009

Where do we draw the line here? Why not just 1 click handler to handle the entire app?

Comment by abickford — November 4, 2009

abickford: I sense your sarcasm :-) but in reality it ends up being a nice way to go. Our event handler (http://github.com/tobowers/motionbox-eventhandler) does just that (but abstracts the handling for you). It’s prototype based but has framework stuff in a module, so someone could modify it for jquery if that makes it easier.

I am with everyone else though – the table (or the body) is where the click handler belongs. I was thinking about scrapping our event handler in favor of jquery live but now I see there’s still a reason for it :-).

Comment by TopperBowers — November 4, 2009

How about the fact that you’re loading 10,000 records in 1 view? This article should be one thedailyajaxian.com instead!

Comment by sixtyseconds — November 4, 2009

I haven’t used jQuery so I’m afraid I’m going to ask this without looking it up in the source, but…

In my own library I use delegation for this kind of thing:

ignite.event.click('.foo',doSomething);

This listens for click events on the document, retrieves the target element, and fires any functions registered against matching selectors. No getElementsByClassName required.

Problems with bubbling / capturing notwithstanding, why does jQuery’s live() not do this? Am I missing something?

Comment by kissmyawesome — November 4, 2009

Catch events on the body, bubbling and delegation is your friend. One star article :(

Comment by deadcabbit — November 4, 2009

It’s bad practice to attach everything to the body when some events don’t even need to be catched past certain parent elements. Imagine checking every mousemove event on the body element when all you need is to check it one UL element. In that case, and that’s most of the time, events are best placed on those parent elements instead for performance.
.
jQuery live() uses this bad practice by attaching everything to the document. It’s best to use something like Prototype’s event.findElement I mentioned here before so you have much better performance.

Comment by Jadet — November 4, 2009

Just to reiterate the point: Event Delegation!

I’m really surprised most of the mainstream libraries have not already implemented Event delegation functionality. As a result when we develop web applications we use reglib along-side jQuery/YUI for event delegation.

Comment by RoryH — November 4, 2009

jQuery’s .live() does use event delegation. See http://docs.jquery.com/Events/live#typefn. I think, the problem here is “where” the event is getting attached. If you attach an event handler to the “child” node, you may potentially hit by performance issues if there are a large number of child nodes. As opined by others here, the trick is to handle events at the parent level. For example:

$(document).ready(function() {
$(‘table’).click(function(event) {
var $thisCell, $tgt = $(event.target);
if ($tgt.is(‘td’)) {
$thisCell = $tgt;
} else if ($tgt.parents(‘td’).length) {
$thisCell = $tgt.parents(‘td:first’);
}
// whatever necessary.
});
});

Comment by ragjunk — November 4, 2009

There are times when event delegation may not be the answer. Imagine you have a 20 or 30 different types of widgets (sliders, tabs, radio/button with indeterminate state, …….) scattered throughout your page. You’re taking a progressive enhancement approach so you don’t attach logic to those widgets (whose operation is independent of the logic that makes the page do useful things) knowing which ones are on the page.

If you used event delegation in this case, you should delegate the code to the body. however, now you have 20 or 30 functions being called for every event. Not particularly efficient.

Event delegation only works for (a) discrete events, that is not things like mouse events (unless you’re insanely efficient with bailing) (b) few variations of behaviour among the descendants and (c) there relatively few descendants, in terms of elements that don’t trigger some behaviour.

There is another option; it won’t address the binding issue but will speed up the process of attaching code to elements. Anyone who went to the Rich Web Experience in 2008 (and probably others since then) probably heard about how Orbitz and ebookers handle adding code when there are tens of thousands of nodes on the page.

It goes something like this (I’m skipping some details to show the technique):


var el, eles = document.getElementsByTagName("*");
for (var i=0, l = eles.length; i<l; i++) {
el = eles[i];
if( el.hasAttribute("Widget")){
WidgetFunctions[el.getAttribute("Widget")]( el );
}
}

You get the idea. The point here is that using a selector is pretty much always going to walk the tree (unless we get native implementation with some heavy classname indexing and you have simple selectors) where as this approach is pretty much linear time. You need to properly handle the attribute checking, and you can be more sophisticated in how you call the Widget function or where it comes from but that’s the basic outline. Get all the element, loop through them looking for an attribute that tells you what to do and then do it.

Comment by littlefyr — November 4, 2009

@littlefyr – Either you or I are misunderstanding Event Delegation. As I understand it, only elements that match a certain selector have the correlating function executed. That means that out of the 20-30 widgets under the delegation, only the applicable functions get fired relating to matched elements.

Also – you don’t need to include every document node in the delegation, or even walk the tree in event delegation, which seems to be a requirement of the code you cite. You only take hold of the element to which the event is attached and the target of the actual event. I’m referring of course to events that have implied targets like mouse and key events.

Aside for how heavy your approach looks on execution time, it also seems needless to have to add and query additional attributes on all child elements in the document. I may be wrong of course…

Comment by sixtyseconds — November 4, 2009

@sixtyseconds.

I’m afraid you misunderstand event delegation.

Lets say you want to trigger a yellow fade effect every time you click on item in an unordered list. “Normally” you would add an onclick event to each of the LI tags. However, events bubble (which is to say that every event on an element triggers the same event on all that element’s ancestors) so instead of putting our on click event on the li, we can put it on the ul. The event will need to be rewritten somewhat but now you have one event binding handling the events for an arbitrary number of descendant elements. The advantage is that if you add items to the list after the fact, you don’t have to add events to those new items since you’ve delegated events for the list item to its parent.

The further you delegate events from element you want to attach behaviour to, the more flexible your code is (you can put those elements in more places without having to change your code) but it also increases the complexity of the code you have to write (especially if elements that have behaviour contain other elements that contain behaviour).

Walking the tree was a reference to how you find the elements to which you are going attach behaviour.

Comment by littlefyr — November 4, 2009

@littlefyr – That’s quite a nice description of event delegation, but then I think we’re on the same page. What I don’t understand how event delegation would be a bad thing for the situation you described in your previous comment. I can’t think of any situation in which the code you provided would be more efficient than delegating the events to all those widgets…

The question becomes – why would you bundle all the event code together for a collection of widgets that could easily change. Surely you would delegate events to each widget container so that the functionality would be self-contained within the widget codebase. Even taking that approach (thereby adding 20-30 events, in the form of delegations) would be more efficient than running your code snippet each time the event occurs…

Comment by sixtyseconds — November 4, 2009

^ I guess you could create some sort of event registry to which your widgets could add their delegates…

Comment by sixtyseconds — November 4, 2009

@littlefyr:
“If you used event delegation in this case, … now you have 20 or 30 functions being called for every event. Not particularly efficient.”

Um, no. You have one function being called for every event that bubbles up to it, which is _ultra_ efficient. Now, it may require more code (in your example) since that one handler may be responsible for handling and/or delegating handling lots of logic to other code. But in terms of efficiency/performance (not code complexity), that’s the way to do it. And for the use case of the OP in this article, using delegation is beyond a no-brainer (not sure what your examples have to do with countering that).

Comment by bmoeskau — November 4, 2009

Fixing live is really easy.

jQuery.extend({
live: function(selector, event, callback) {
var jq = $(document);
jq.selector = selector;
jq.live(event, callback);
}
});

Then just use $.live('.someClass', 'someEvent', someCallback); No expensive selection, full proper event delegation.

Comment by eyelidlessness — November 4, 2009

Oops. In my fix, you should probably add return jq; to the end of the live extension.

Comment by eyelidlessness — November 4, 2009

@eyelidlessness: That’s no fix, live still checks everything on the document even when some events don’t have to be catched there. live is just not the best way to delegate events. It’s made for ease, if performance was kept in mind there wouldn’t be a live. The proper way to delegate is on parent elements as many have pointed out and ragjunk showed with his snippet, not by doing everything on the document like live does it. jquery could benefit from having a Prototype like event.findElement to allow for a fine grain of control while having maximum performance, it would look like this:

findElement: function(event, expression) {
var element = event.target;
if (!expression) return element;
return $(element).closest(expression)[0];
}
...
// Use it like this. With live() this would be overkill
$(document).ready(function() {
$('#parentElement').bind('mousemove', function(event) {
var element = event.findElement('li div.remove');
if (element) { /* do stuff */ }
}
});

Don’t waste your time trying to fix live().

Comment by Jadet — November 4, 2009

Folk,
newer Firefox and Webkit nightlies are trying to solve these problems for you tomorrow ! They implement new selectors matcher API:

webkitMatchesSelector() for Webkit browsers
mozMatchesSelector() for Firefox browsers

in the mean time NWMatcher has a match() method, working on browsers that do not have and will never have the above method and it does as fast (or faster) as those new API even on old browsers.

This is what you are looking for, delegation is good, it just doesn’t fit too well with the select() method and binding / wrapping way in current libraries. The match() method is still not appreciated enough by devs, but they will be pushed to use that for complex situations like the one described above.

For event deleagtion I suggest you have a look at NWEvents which is the companion of NWMatcher, these were built as a combo to solve this exact problem. Here is a demo of it’s capabilities:

http://javascript.nwbox.com/cljs-071809/nwapi/nwapi_test.html

Contrary to what is said above this example handles mouseover/mouseout on complex selectors (:nth-of-type) quite efficiently and if you can have a peek at a profiler session you will quickly understand the requirements for the browsers are very low.

Works on any browser IE6 too.

The reason this can be done is the incredible speed of the underlying match() method for which you can see speed benchmarks here on the FuseJS site:

http://www.fusejs.com/nwmatcher/match/

The nightly build tests only appears if you have a nightly build to test ;-)

Prototype has already released an adapter for NWMatcher in their current code base, it will be publicly available really soon, I am really looking forward for the outcome of delegation in that wonderful lib.

The suggestion to set an ID on the table and attach a single event to it is still the best pure solution for a one shot trick that solves, if you need to apply this pattern over all your projects then NWEvents+NWmatcher are very helpful.

Comment by dperini — November 4, 2009

@dperini – Since we’re using this as a forum to promote libraries, let me mention that MooTools also has a match() method as well as a passable event delegation model that addresses the issues mentioned here. NWMatcher isn’t the first or only solution to the problem. :)

Comment by sixtyseconds — November 4, 2009

Jadet,

I didn’t mean to imply that it was fixing all potential issues with $.live, just that it fixes the issue with needlessly polling a selector before attaching to document. It’s an issue that can be solved in a general sense, while what you’re proposing requires more careful planning and knowledge of the document structure. That said, there’s no reason live couldn’t be further extended to allow a parent node and child selector together, but it still wouldn’t give you the finer-grain control you’re probably looking for. For smaller projects, $.live is great; for more complex work, obviously you’ll want to roll your own.

Comment by eyelidlessness — November 4, 2009

@sixtyseconds,
no doubt NWMatcher has been the first to do that in a wrapped implementation (you can easily check dates and all), it is true I am not the only one (base2 IS a real library and does that very fast too in JSB).

I don’t have a library myself sorry, however I will be glad to see a speed benchmark on that functionality of Mootools (just to have an indication of what you say).

Both the code presented here and jQuery (and I believe Mootools too) are still depending on a “onload” notification to be able to initialize stuff on the page, NWMatcher avoids that problem completely by letting you pre-declare event binding before the elements really exists on the page.

This has the enormous advantage of avoiding the infamous “onload” problem and/or the “ready” event, they are simply not needed anymore and will allow for a real cross-browser early page enlivenment.

Comment by dperini — November 4, 2009

@dperini – You cannot honestly say that at no stage do you need to wait for the DOM to load before attaching events to elements can happen. They simply do not exist before domload. Sure enough; the big players like YUI and jQuery let you attach the events yourself, and maybe NWMatcher does that for you, but behind the scenes all the libraries are doing the same things and are bound by the same rules.

Be that as it may, I only wish/ed to say that there are many ways to do this and we can all agree that the issues in the article would best be resolved using event delegation. Nobody seems to taken issues with the mention of loading 10,000 records initially though – isn’t that a horrible usability concern?

Comment by sixtyseconds — November 4, 2009

In my experience the best performing solution is to put onclick=”…” in the tr tag html. This performs significantly better with 100s of elements – you don’t even need 1000s (although maybe the difference isn’t as significant if you use chrome, or a fast computer). If you generate the page, why would you plan on making the browser do a significant amount of post-processing after the page is loaded/rendered?
Sure, a simple css selector and bind is elegant, but it’s hard to expect 10k dom manipulations to perform well. Just make the page the way you like it in the first place and bind stuff dynamically only as needed.

Comment by paulbo99 — November 4, 2009

@sixtyseconds – The document.documentElement is there ;D

Comment by jdalton — November 4, 2009

@jdalton – How right you are! We learn new things every day :D

Comment by sixtyseconds — November 4, 2009

@sixtyseconds,
Yes … I can honestly say that NWEvents works like that, even if it is hard to believe it:

NW.Event.appendDelegate(‘ul#nav li:nth-child(even)’, ‘mouseover’, fn);

and you can execute that as the very first line of your script without the need to wait for “onload” or “ready” events. The code to load to make this work is about 3Kb.

So it is not just the delegation problem you get solved but a lot of other nasty “non cross-browser” behaviors like “onload/ready” events.

Comment by dperini — November 4, 2009

@dperini – Yes – jdalton explained it to me :D
Do you think it’s the most efficient way to handle things considering you would essentially wait for ALL events to bubble up to documentElement? Surely very few, if any, need to bubble that far…I think I wondered though – the issue in this article was more about delegation than it was about domready vs. NWMatcher. Good work though.

Comment by sixtyseconds — November 4, 2009

@sixtyseconds
int the code you have the choice to not use the “documentElement” if that doesn’t fit your requirements, but then you will need to wait “onload/ready”, at that time you can add a last argument to the above example which will be the “parent” node you would like to listen on, to shorten the bubbling path.

As a bonus, form events like submit/focus/change can be used with delegation as any other element event and cross-frames events.

Capturing of events is available also for IE, with those I am trying to cover Aria/A11Y compliance.

Comment by dperini — November 4, 2009

Nice work there Diego. You mentioned not having a framework, why not write your own and use NWMatcher in there? Who knows, maybe some frameworks will pick up on a thing or two ;)

Comment by Jadet — November 4, 2009

I was a little confused at first about the nature of this article. I had the same “ajaxian, where’s the event delegation” reaction that everyone else had. But I did learn something from this post, namely the added unload time created by event garbage collection. Never would have thought to measure that.

Comment by jlizarraga — November 4, 2009

Hey folks,
Here’s the link to my original article:
http://blogs.atlassian.com/developer/2009/08/jquery_bondage.html

At the end of the day, we chose to use jQuery’s live because:
1. it was easy to use, and fit in well with our existing jQuery infrastructure
2. we didn’t see the need to “roll our own” because at the end of the day, using live worked well enough for our usages

Comment by sebr — November 4, 2009

I was among the others with a quick patch for .live (http://blurf.furf.com/2009/09/jquery-live-from-new-york/), inspired by Paul’s preso at jqcon in Boston. But on my flight to Berlin for jsconf, I decided the best way to get it into the framework for reals was to patch it for reals and put .live() where it belongs. So here’s my external patch, which can become a real patch with two copies and two pastes: http://github.com/furf/jquery-live/blob/master/jquery.live.js

Comment by furf — November 5, 2009

@furf, I’ve been trying to convince jQuery that live should be something like:

$(‘table’).delegate(‘tr’,'click’,func);

Btw, JavaScriptMVC’s delegator plugin provides this for all common events (submit) much faster than jQuery.

Comment by JustinMeyer — November 10, 2009

Hey,

It’s a nice tutorial to understand jquery.

I have found another great tutorial site with almost all the information of jquery in php.

http://phpschools.freehostia.com

Comment by yashraj2 — June 23, 2010

Leave a comment

You must be logged in to post a comment.