Problem: I need to bind any number of event handlers to any number of elements (DOM nodes, window, document) at dynamically runtime and I need to be able to update event binding for dynamically created (or destroyed) nodes during the lifetime of my page. There are three options that I can see for tackling this problem:
I) Event delegation on window
II) Direct event binding on each node
III) Event delegation on common ancestors (which would be unknown until runtime and would potentially need to be recalculated when the DOM is altered)
What is the most efficient way of doing this?
A little context
I am working on a set of pages that need analytics tracking for user events (clicks, scrolling, etc.) and I want to be able to easily configure these event handlers across a bunch of pages without needing to write a script to handle the event binding for each instance. Moreover, because I may have the need to track new events in the future, or to track events on elements that are dynamically added to/removed from the page, I need to be able to account for changes in the DOM that occur during the lifetime of the page.
As an example of what I'm currently considering, I would like to create a function that accepts a config object that allows the programmer to specify default handlers for each event, and allow them to override them for specific elements:
Analytics.init({
// default handlers for each event type
defaultHandlers: {
"click": function(e) { ... },
"focus": function(e) { ... }
},
// elements to listen to
targetElements: {
// it should work with non-DOM nodes like 'window' and 'document'
window: {
// events for which the default handlers should be called
useDefaultHandlers: ['click'],
// custom handler
"scroll": function(e) { ... }
},
// it should work with CSS selectors
"#someId": {
useDefaultHandlers: ['click', 'focus'],
"blur": function(e) { ... }
}
}
});
Sources
SO: Should all jQuery events be bound to document?
SO: How to find the nearest common ancestors of two or more nodes
jQuery docs: $.fn.on()
I usually delegate events on the document.documentElement object because:
It represents the <html> element on the page, which holds everything which holds all the HTML tags the user can interact with.
It is available for use the moment JavaScript starts executing, negating the need for a window load or DOM ready event handler
You can still capture "scroll" events
As for the efficiency of event delegation, the more nodes the event must bubble up the longer it takes, however we're talking ~1 to 2 ms of time difference -- maybe. It's imperceptible to the user. It's usually the processing of a DOM event that introduces a performance penalty, not the bubbling of the event from one node to another.
I've found the following things negatively affect JavaScript performance in general:
The more nodes you have in the document tree, the more time consuming it is for the browser to manipulate it.
The greater the number of event handlers on the page the more JavaScript slows down, though you would need 100s of handlers to really see a difference.
Mainly, #1 has the biggest impact. I think trying to eek out a performance boost in event handling is a premature optimization in most cases. The only case I see for optimizing event handling code is when you have an event that fires multiple times per second (e.g. "scroll" and "mousemove" events). The added benefit of event delegation is that you don't have to clean up event handlers on DOM nodes that will become detached from the document tree, allowing the browser to garbage collect that memory.
(From the comments below) wvandell said:
The performance costs of event delegation have little to do with the actual 'bubbling' of events ... there is a performance hit incurred when delegating many selectors to a single parent.
This is true, however let's think about the perceived performance. Delegating many click events won't be noticeable to the user. If you delegate an event like scroll or mousemove, which can fire upwards of 50 times per second (leaving 20 ms to process the event) then the user can perceive a performance issue. This comes back to my argument against premature optimizations of event handler code.
Many click events can be delegated with no problem on a common ancestor, such as document.documentElement. Would I delegate a "mousemove" event there? Maybe. It depends on what else is going on and if that delegated "mousemove" event feels responsive enough.
Related
Let's say I have bunch of click events. Also one/few of them is for document object.
Which one is better for performance? Click event for each element or :
document.addEventListener('click', (e)=>{
if(e.target == firstObject){ firstFunction(e) }
if(e.target == secondObject){ secondFunction(e) }
if(e.target == ThirdObject){ thirdFunction(e) }
})
Neither is "better." They each have their place in your toolkit.
A single delegated handler is more complex in that you have to do the kind of dispatch you're doing in your example (often using closest or matches), but has the advantage that if you're adding/removing elements you want to act on, you don't have to juggle event handlers.
Directly-assigned handlers are simpler (at least on elements that aren't added/removed), can prevent propagation, and let you keep your code more modular, more in keeping with the single responsibility principle.
Use the one that makes the most sense in a given context.
I think event listener for each element is better if possible, and makes sense in terms of code quality. There are some cases though where a document event listener will be needed ( for example to emulate a click outside behaviour)
That being said here are some of reasons that makes event listener for each element a better solution
event propagation is handled for you by the browser, if you decide to have only one event handler for the whole document, and u want to have event listeners for elements that are contained in each other, then you will need to handle propagation your self. That is to say you need to handle the order in which functions run yourself, and then you will have some either complex generic solution, or a specific imperative verbose code with a lot of if else statements.
Easier to read code, this is even more true for recent frameworks for web like react, angular, etc..., so for example assume you want to have a listener for clicks on the document, where that code should reside, in which file, and which component should own the code.
Removal of event listeners is handled for you by the browser apis, the browser gives you a way to remove event listeners. If you decide to go with a global event listener then you should handle removing event listeners yourself.
Your code will be hard to refactor and easier to break later, because you are coupling your document (or container ) event listener to your components internals. That is if you decide to change the structure of these components later, your document based event listener will probably break. This will depend a lot on how you identify the target of clicks, for example if you were identifying them by class names or other attributes, then these attributes might change later for reasons like styling.
and if you depend on ids for example you might eventually have unexpected results. because what happens for example if you added a listener for an element that has id, removed that element, and then later added another element with same id.
You miss on the development tooling provided for you by browsers, browsers can show you attached listeners for elements, with a document based event listener you wont be able to do that
It's better if you add one by one, because then you can remove event whenever it finish. Moreover you have more control about this event.
On a standard webpage you have hundreds (or thousands) of DOMNodes. When building single-page applications (or angular/react/polymer based projects) almost every link or button is already click-jacked by the framework using custom events to bind the DOMElement to a handler function.
So, since you can bind the whole document to a click event (and then check the event.target) would that be a more performant way to listen for lots of events?
Or is setting dozens of click handlers still better?
function report(event) {
console.log(event.target, event);
return false; // You would only want to return false if you actually wanted to intercept this event (and the user wasn't pressing ctrl/cmd+click).
}
document
.addEventListener('click', report, true);
// vs
document.querySelector('.a')
.addEventListener('click', report, true);
document.querySelector('.b')
.addEventListener('click', report, true);
document.querySelector('.c')
.addEventListener('click', report, true);
More performant can mean less CPU cycles or less memory or both.
There are multiple reasons binding to the document (called event delegation) is better:
There are fewer instances of the event handler in memory. If you have very large numbers of items, this will save memory
There is a smaller chance of memory leaks for event handlers that do not get unregistered properly when elements are deleted
There is less work that has to happen every time you add a new element of the type that should respond to the event handler - hence less CPU usage
Depending on the application, the delegated event handler might have to do more work to figure out how to process the event properly for each individual instance, whereas the individual handlers can use the scope to store values for use inside the handler itself - this might be quicker.
So the answer depends on your application
Yesterday I was reading the jQuery docs for .on() where was stated:
Avoid excessive use of document or document.body for delegated events on large documents
But today, I was looking at this JSPERF and I notice a better performance when the click event is attached to the document.
So right now, I'm confused. The performance tests speak against the docs?
Your JSPerf here is testing the speed to attach events, not the effect that they have on cumulative page performance. This is the wrong thing to test!
Javascript events propagate up the DOM all the way to the document root. This means that if you have an on("click", ...) handler on document, then every click on every element in the document will end up running an event handler, so jQuery can test if its origin matches the delegate target, to see if it should be passed to that event handler.
Imagine that your page has 10 different delegated event handlers on document, all handling various clicks. Every time you click any element in the page, the event will bubble up to the document root, and all 10 of those handlers have to be tested to figure out which (if any) should be run.
In general, you want your delegated events to be as deep in the tree as possible while still enabling your functionality, since this limits the number of elements that may invoke this event, and you can handle the event earlier to prevent it from propagating up the DOM tree.
It depends.
You can attach handler to any element you want, of course, and in some cases you will have to attach it to document or body (if you, for example, want to target all the links on the page). But, if you are sure that certain elements will always appear only inside given element (which is already created) - then for performance sake, you can attach event handler to that common parent.
The point is excessive.
IMHO excessive delegates on any DOM is terrible
The pagination controls on a page I am working on were being bound conditionally on there being more than 1 page. I don't like to see the following code in my projects,
if (pages > 1) {
$('.some_class').bind('event', function() {});
}
because I feel it represents a disorganized coding style. I would put it on the same level as sprinkling return statements here and there rather than using control. I feel like binding events to globally available objects has no place in the local scope of a function call. So what I usually do is make two javascript files, for example: pagination.js and pagination-controls.js. In the one I have logic about building the html and displaying the the pagination controls. In the other I have statements like the following:
$(document).on('click', '.pagination .next', function() {});
Which fires regardless of whether there is a $('.pagination .next') element anywhere on the page. I like the way that feels: the website has behaviours and it only knows about ids and classes, not about instance variables in some local scope somewhere.
EDIT: this is definitely bad practice, as mentioned below. However:
As of jQuery 1.7, the .on() method is the preferred method for
attaching event handlers to a document.
and the discussion on direct and delegated events is relevant. In particular I think the following describes my usage:
By picking an element that is guaranteed to be present at the time the
delegated event handler is attached, you can use delegated events to
avoid the need to frequently attach and remove event handlers. This
element could be the container element of a view in a
Model-View-Controller design, for example, or document if the event
handler wants to monitor all bubbling events in the document.
EDIT: So I guess now I'm wondering "is it bad to prefer binding behaviours to parent elements unconditionally over binding based on logic?" That's perhaps just a question of style, and my original question has been answered so I think I will accept the answer.
Yes, this is causing significant unnecessary overhead, and it is a "bad practice".
Binding your event handling to the top-level document object means that every single click that occurs on any element anywhere in your page will bubble up to the document object, where the event's target is checked to see if it matches .pagination .next.
In fact, the documentation itself recommends against your usage:
Attaching many delegated event handlers near the top of the document tree can degrade performance. Each time the event occurs, jQuery must compare all selectors of all attached events of that type to every element in the path from the event target up to the top of the document. For best performance, attach delegated events at a document location as close as possible to the target elements. Avoid excessive use of document or document.body for delegated events on large documents.
So, you're misusing on. It's for binding directly to elements or to parent elements which may have dynamically created children, and you are meant to bind to the closest possible parent element. Binding to the document is certainly not meant to be the only way you handle events in your page.
If I have an element (html) nested in another element and both of them have a click handler attached, clicking the inner element executes its click handler and then bubbles up to the parent and executes its click handler. That's how I understand it.
Do events bubble up the DOM tree if there are no events attached that are the same and if so, is it worth putting a event.stopPropagation() at the end of every handler to stop this and speed things up?
events almost always bubble up unless event.cancelBubble=true is set or event.stopPropagation() is used. You are only aware of it, though, when one of your event
handlers gets tripped.
See http://en.wikipedia.org/wiki/DOM_events for a list of events which bubble. (Note: in the table of HTML events, cancelable refers to the effectiveness of event.preventDefault() or return false to cancel the default action, not bubbling)
Also see http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow, in particular 1.2.1 Basic Flow to understand the capture phase and bubbling phase of event propagation.
EDIT
http://mark-story.com/posts/view/speed-up-javascript-event-handling-with-event-delegation-and-bubbling suggests there is a performance gain by stopping propagation but provides no data.
http://groups.google.com/group/Google-Web-Toolkit/browse_thread/thread/a9af0aa4216a8046 suggests that browsers should be optimized for bubbling behaviour and says there should be no significant performance difference. Again no data.
http://developer.yahoo.com/performance/rules.html#events provides a good technique for improving event-handling performance, but doesn't directly talk about stopPropagation performance.
Ultimately, you'd have to profile the difference to get a good idea of the benefits on your site.
I suppose this behavior is already well optimized by browsers, so you won't be able to catch significant performance boost when stopping propagations (except, perhaps, for really-really complex nested DOM structures). If you are worried by performance and deal with lots of events, you may be interested in event delegation instead.
Also, you should remember your code should stay readable and self-explainable. stopPropagation() is a method used for certain purpose, so using it in every method could be really confusing.