Why not take Javascript event delegation to the extreme?

By now most folks on this site are probably aware that:

$("#someTable TD.foo").click(function(){
    $(e.target).doSomething();
});

is going to perform much worse than:

$("#someTable").click(function(){
    if (!$(e.target).is("TD.foo")) return;
    $(e.target).doSomething();
});

Now how much worse will of course depend on how many TDs your table has, but this general principle should apply as long as you have at least a few TDs. (NOTE: Of course the smart thing would be to use jQuery delegate instead of the above, but I was just trying to make an example with an obvious differentiation).

Anyhow, I explained this principle to a co-worker, and their response was "Well, for site-wide components (eg. a date-picking INPUT) why stop there? Why not just bind one handler for each type of component to the BODY itself?" I didn't have a good answer.

Obviously using the delegation strategy means rethinking how you block events, so that's one downside. Also, you hypothetically could have a page where you have a "TD.foo" that shouldn't have an event hooked up to it. But, if you understand and are willing to work around the event bubbling change, and if you enforce a policy of "if you put .foo on a TD, it's ALWAYS going to get the event hooked up", neither of these seems like a big deal.

I feel like I must be missing something though, so my question is: is there any other downside to just delegating all events for all site-wide components to the BODY (as opposed to binding them directly to the HTML elements involved, or delegating them to a non-BODY parent element)?


Solution 1:

What you're missing is there are different elements of the performance.

Your first example performs worse when setting up the click handler, but performs better when the actual event is triggered.

Your second example performs better when setting up the click handler, but performs significantly worse when the actual event is triggered.

If all events were put on a top level object (like the document), then you'd have an enormous list of selectors to check on every event in order to find which handler function it goes with. This very issue is why jQuery deprecated the .live() method because it looks for all events on the document object and when there were lots of .live() event handlers registered, performance of each event was bad because it had to compare every event to lots and lots of selectors to find the appropriate event handler for that event. For large scale work, it's much, much more efficient to bind the event as close to the actual object that triggered the event. If the object isn't dynamic, then bind the event right to the object that will trigger it. This might cost a tiny bit more CPU when you first bind the event, but the actual event triggering will be fast and will scale.

jQuery's .on() and .delegate() can be used for this, but it is recommended that you find to an ancestor object that is as close as possible to the triggering object. This prevents a buildup of lots of dynamic events on one top level object and prevents the performance degradation for event handling.

In your example above, it's perfectly reasonable to do:

$("#someTable").on('click', "td.foo", function(e) {
    $(e.target).doSomething();
});

That would give you one compact representation of a click handler for all rows and it would continue to work even as you added/removed rows.

But, this would not make as much sense:

$(document).on('click', "#someTable td.foo", function(e) {
    $(e.target).doSomething();
});

because this would be mixing the table events in with all other top level events in the page when there is no real need to do that. You are only asking for performance issues in the event handling without any benefit of handling the events there.

So, I think the short answer to your question is that handling all events in one top level place leads to performance issues when the event is triggered as the code has to sort out which handler should get the event when there are a lot of events being handled in the same place. Handling the events as close to the generating object as practical makes the event handling more efficient.

Solution 2:

If you were doing it in plain JavaScript, the impact of random clicks anywhere on the page triggering events is almost zero. However in jQuery the consequence could be much greater due to the amount of raw JS commands that it has to run to produce the same effect.

Personally, I find that a little delegation is good, but too much of it will start causing more problems than it solves.

Solution 3:

  • If you remove a node, the corresponding listeners are not removed automatically.
  • Some events just don't bubble
  • Different libraries may break the system by stopping event propagation (guess you mentioned that one)