Speed up javascript event handling with event delegation and bubbling

Events are the bread and butter of interactive websites. However, attaching and detaching events to the DOM can create memory leaks and performance issues due to time spent attaching events. This normally is not an issue, unless you are binding a large number of events. This problem is further minimized by modern javascript libraries ability to queue event binding operations and process them when the coast is clear. However, this leaves room for uneventful user interaction while the browser attaches 100’s or 1000’s of events.

Thousands of events?

Yes thousands of events. This may not be a common occurrence, but when you start dealing with large volumes of events you quickly run into some troubles. You are either faced with long halting waits as events are attached, or an interface that is partially functional while events are taken out of the queue. Thankfully Javascript has built-in features to make dealing with this situation easy. For example if you have a set of nested lists with several hundred branches you may end up binding 100’s events. Instead of attaching events to every <li> that has a child <ul> you can bind one event to the root <ul> element and allow the events from the branches to bubble up.

Dealing with bubbling events

Since most events bubble up the DOM. We can capture events that originate in a child <li> by attaching a listener on a parent element, and delegate the event response. If you are not familiar with event bubbling, most user events will trigger the same event on every parent in the DOM tree, until either the event is cancelled or it reaches document. Some events do not bubble (focus, blur, load, unload) so event delegation will not work with these event types. However, with compatible events binding a single event to the <ul> will automatically capture any and all events occurring lower in the DOM tree. When events reach the parent listener a few things will be different. First this will be the element the listener is attached to, not the element the event originated on. To get to the original element we need to check event.target in standards browsers and event.srcElement in Internet Explorer. Most libraries unify this property to event.target, but check how your favorite Javascript library handles it. Once you have the element the event originated on you can traverse the DOM and do any manipulation needed just like normal.

Show Plain Text
  1.  
  2. function _delegateNeatArray (event) {
  3.     var clickedEl = event.target;
  4.     while (clickedEl.nodeName.toUpperCase() !== 'LI') {
  5.         clickedEl = clickedEl.parentNode;
  6.     }
  7.     var subUl = clickedEl.lastChild;
  8.     var hide = Boolean(subUl.style.display === 'block');
  9.     if (hide) {
  10.         Element.hide(subUl);
  11.         Element.swapClass(clickedEl, 'expanded', 'collapsed');
  12.     } else {
  13.         Element.show(subUl);
  14.         Element.swapClass(clickedEl, 'collapsed', 'expanded');
  15.     }
  16.     if (event.stopPropagation !== undefined) {
  17.         event.stopPropagation();
  18.     }
  19. }
  20.  

This is an example of an event handler that uses event bubbling and delegation from “DebugKit’s“http://thechaw.com/debug_kit newly rebuilt Javascript. This handler handles the click events on the collapsible array lists. Before switching to event delegation, pages were attaching between of 100-900 events on an application I’m working on. After switching to Event delegation only 5 events were attached, one for each collapsible list, greatly reducing the work done for the toolbar. One thing to note about this type of event handling, is that you must be prepared for any child element to be the target of the event. In the case of DebugKit Events often come from the <strong> inside the <li> I used basic tree traversal to find the element I’m actually interested in which is always the <li>.

Show Plain Text
  1. var clickedEl = event.target;
  2. while (clickedEl.nodeName.toUpperCase() !== 'LI') {
  3.     clickedEl = clickedEl.parentNode;
  4. }

Will traverse the DOM upwards looking for an <li> element. Once one is found event handling continues.

Popping the bubbles

As mentioned earlier, events will bubble until they are canceled or reach document. To cancel an event you use different properties based on the browser, in standards based browsers you use the stopPropagation() method of the event object. With Internet explorer you need to set the cancelBubble property to true. Doing so will prevent further event bubbling. Pretty much all javascript libraries offer a way to unify the cancelBubble/stopPropagation differences so check how your library handles it.

The good and the bad

As said before, there are a few snags in using this otherwise awesome approach.

* Not all events bubble, so this technique doesn’t work with all event types. Other events such as mouseout are extra tricky as well. * It can make event code slightly more complicated as you need to resolve the correct target.

But on the plus side:

* There is the potential for huge performance gains both in time, and reduction in potential memory leaks. * You don’t need to rebind events if elements change. Since you capturing events in the parent element, you can update the child elements with Ajax without having to worry about rebinding events. This is another performance gain that is amplified by volume.

So may all your events bubble, and I hope you can use this powerful and efficient technique the next time you hit the wall with events.

Comments

It’s a nice quick overview of event delegation but you should point out the IE differences, since the event object has different properties to capture the target and to cancel bubbling.

Jonathan Snook on 8/4/09

Jon: Good point, I’ve updated the article to tell about target/srcElement and stopPropagation()/cancelBubble.

mark story on 8/4/09

Great article, Mark. I learn something new every time I come to your site.

Jon on 30/4/09

Thanks, just what I needed for to help me progress in my task

HighFlying on 7/6/12

Have your say:

*
* You can use Textile markup, but be reasonable