jQuery Event Delegation

22 June 2011

A lot of words have been published about the various methods jQuery provides to bind event handling code to specific elements in the DOM. In the very beginning there was only bind() and its plentiful shortcuts. Starting with the 1.3 release, native support for delegation-based event handling was added. The difference is that traditionally you attach event listeners to individual DOM elements, whereas with delegation you register handlers with a central event listener higher up the DOM hierarchy.

The benefits of the event delegation approach have been described in abundance, so I'll keep it brief: First, your handlers also get called for elements added dynamically after you've set up the event handler. Second, if you have many elements on a page that handle the same events in the same way, not having to attach the handler to each element individually is more efficient.

At first glance, the difference between using live() versus using the newer delegate() function seems rather subtle: the former registers the event handler at the document level (at least by default), while the latter can be used to register with an event listener on any element in the DOM.

So you may think that you should really use live() if you want to handle those events at the document level. However, as I found out the hard way a while ago, that can become a performance bottleneck, especially on browsers with lackluster selector and traversal performance (that is, basically, Internet Explorer). I will try to explain why that is by looking into the mechanics triggered by those innocently looking few-liners.


Let's start with a simple example to demonstrate the usage of the live() function. We'd like to open all links with the class “external” in a new window. Should be rather trivial:

$("a.external").live("click", function() {
  return !window.open(this.href);

What this does is that it creates a jQuery object with the selector “a.external”, and invokes its live() method to register a handler for “click” events. On the surface this invocation style seems to be consistent with the rest of the jQuery API. Under the covers however, there's some strange magic going on to actually implement event delegation: The live() method doesn't actually look at the jQuery object in terms of the elements that match the selector, it just looks at the selector itself. So while the jQuery team tried to make this method look like a variation of the traditional bind() method, it doesn't actually work in a comparable way at all (plus it doesn't play well with method chaining).

Okay, but what does that have to do with performance? Note that the first thing we did in the code snippet above was to create a jQuery object. While the live() function only really looks at the selector, this still has the side effect of starting up Sizzle to find all elements in the document that match the given selector. For large documents combined with browsers that do not support document.querySelectorAll(), that's a whole lot of DOM traversal. The result of this is discarded, so it's basically a lot of work done for nothing. And you're probably doing this on page load or at some other point where you really don't want to add any unnecessary delays.

And whenever the event is propagated up to the event listener at the document level, the selector engine is run again anyway to find whether the target element of the event matches the selector. This time the selector check is a lot more efficient though, as it doesn't collect all matching elements in the context, but just checks whether the the selector matches some element node in the tree between the context element and the event target. Internally, jQuery does the following to dispatch events to live handlers:

$(event.target).closest(selectors, event.currentTarget);

Starting at the clicked element, this walks up the tree up to the context element and evaluates the selectors of all live event handlers registered with that context.

live() with Context

If you know the jQuery API well, you may point out that the scope of the selector matching can be reduced to a specific “context” by passing an additional argument to the jQuery constructor.

var ctxt = $("#content")[0];
$("a.external", ctxt).live("click", function() {
  return !window.open(this.href);

With this change, the DOM traversal is restricted to the given context (in this case an element with the ID “content”), and the event listener is attached to that context element instead of to the document.

However, this does not change that the selector engine kicks in and wastes precious cycles.


jQuery 1.4.2 introduced the delegate() and undelegate() methods, which address this problem. With delegate() you do not create a jQuery object just to specify the selector for the event handler. Instead the selector is just a string argument to the method, and the context is specified by the jQuery object that the method is invoked upon:

$(document).delegate("a.external", "click", function() {
  return !window.open(this.href);

This is the event delegation API that should have been there in the first place. It only evaluates the selector when events are fired, doesn't involve strange magic, and doesn't break method chaining.

Let Live Die

I am probably not the only jQuery user who has naïvely used live() just because it looked a bit simpler, and because the disadvantages are not fully described in the documentation. I hope that method will be explicitly deprecated in favor of delegate() and phased out in some future version.

In the meantime, whenever you want event delegation, use the delegate() method, even if the context is the document.


No comments or pings so far.