Event delegation works by attaching a single event listener to a parent element to catch events bubbling up from the children. Many people believe this is more performant than attaching event listeners to each child. I am not convinced this is always true.
Let's start with a common example of event delegation. Here we have a list of elements:
<ul id="item-list">
<li data-cost="12">Item 1</li>
<li data-cost="18">Item 2</li>
<li data-cost="6">Item 3</li>
...
</ul>
We want to log the item cost when the user clicks an item. In order to do that, we loop through every item and add a unique event listener that reads the data-cost
attribute:
for (const li of document.getElementById('item-list').children) {
li.addEventListener('click', () => console.log(li.dataset.cost));
}
However, there is an issue with this code. Looping over an unknown amount of items can add additional processing time before the page is interactive and adding an event listener to every li
will increase memory usage unnecessarily. To avoid that, we can add a single event listener to the ul
that references the li
using event.target
:
document.getElementById('item-list').addEventListener('click', event => {
console.log(event.target.dataset.cost);
});
That works, but what if our li
elements become a little more complicated?
<li data-cost="12">
<div>Item 1</div>
<div class="description">Item one's description</div>
</li>
event.target
might reference one of the li
element's children depending on where the user clicks. This can be handled by searching for the closest parent that matches the li
or checking the event's path. Downside is, this requires the event listener to account for the structure of your HTML.
Let's go back to the original solution and understand why attaching event listeners to every element is not performant. There are three parts to this theory:
- Looping over a large set of elements adds additional time before the page is interactive.
- Adding an event listener for every element slows down the page due to some overhead cost of having event listeners attached.
- Memory usage is unnecessarily increased because a new function is created for every event listener.
The first argument is valid but only with extremely large sets of elememnts. A low-end smartphone user wont notice the difference between 10 and 1000 elements being looped over. There may be issues once you reach tens of thousands of elements, but the cost of sending that HTML and the browser rendering it will far outweigh the 100ms+ cost of attaching event listeners.
The second argument might have been true at one time, but modern browsers do not suffer from having many event listeners attached. The performance overhead for having an event listener attached is only realized when the specific event is dispatched, not when idle or other events are dispatched.
This third argument is valid, but simply reusing the same function for every element solves the issue:
const listener = event => console.log(event.currentTarget.dataset.cost);
for (const li of document.getElementById('item-list').children) {
li.addEventListener('click', listener);
}
Event delegation does have an advantage by handling elements added dynamically during the lifetime of a page but you should consider attaching event listeners when the element is created. This reduces implicit coupling between elements (ul
and li
in the example).
Another bonus is an improvement when debugging. Some browser debugging tools allow you to see the event listeners attached from the elements panel. Having the event listener directly attached to the element can be a time saver.
So, does event delegation actually improve performance?
Yes, by not looping over an undefined number of elements, the page's time to interactivity is reduced. It could save you one millisecond or a thousand. Do some testing to see if it's really worth it.
thank you!