Events are the most common way to promote interactions between the UI and application layers on web applications. Be it a click, a focus or a scroll, webapps are filled with them, bringing sites to life, enhancing user experience and promoting friendly interfaces to the end user.

As much as they are helpful, event handlers are costly. Sometimes very costly.

The Problem

Let's use a very simple situation which you have surely already encountered on one of your applications. Assuming you have a list with a variable number of items and you want to set a click event, you might have implemented something like this:

let rings = ['elves', 'dwarves', 'men']; 

let rule = (evt) => { 

  let race = evt.target.textContent;

  console.log(`you rule the ${race}`);

};

 

let drawRings = (rings) => { 

  let ringList = document.querySelector('#ringList');

 

  rings.forEach((ring) => {

    let ringEl = document.createElement('li');

    ringEl.innerText = ring;

    ringEl.addEventListener('click', rule);

    ringList.appendChild(ringEl);

  });

};

 

drawRings(rings); 

See live demo

Note: appendChild(), as any DOM manipulation operation, is slow. Using it on a loop is not recommended for a large amount of items (you might want to use Document Fragment instead). We are going with append just for the sake of simplicity.

Although this code works, we are binding one handler for each element. That means we are allocating space in memory for every single item being displayed.

Now imagine a commerce site listing hundreds of products and each one of them having their own set of clickhover, and other events, and it's easy to see where we are heading.

How We Can Solve It

Our goal is to have as few event handlers as possible, and Javascript has in its fundamentals the right tools to help us with that. The answer is event delegation. But first, let's take a look at some concepts.

Event Bubbling

The way events work on Javascript is very simple. It's fired from the innermost captured element, and then bubbled up to outside elements. Also, event handlers are stacked so they will be executed one at a time from the captured (inner) to their last parent (usually document). The code below illustrates that behavior:

let clickHandler = (evt) => { 

  let tagName = evt.currentTarget.tagName;

  console.log(`executing handler from ${tagName}`);

};

 

document.body.addEventListener('click', clickHandler); 

let outer = document.querySelector('.outer'), 

    text = document.querySelector('.text'),

    inner = document.querySelector('.inner');

 

outer.addEventListener('click', clickHandler); 

text.addEventListener('click', clickHandler); 

inner.addEventListener('click', clickHandler); 

See live demo

So... If the event is fired to outer elements, that means we don't need to assign our handler to each one of the inner ones, right? Right!

But we aren't we missing a final piece? We already know a click event was triggered, but our handler needs to identify the source element so it can act properly and individually. Thankfully, Javascript makes it easier by keeping a reference of target object:

var target = evt.target; // Returns the Node object that originally fired the event

The target property of an event object ensures that we will always have a reference for the element that dispatched the event we are listening to. Its value is the same all the way up the chain, enabling us to provide specific behavior and handlers.

That's what we call event delegation. We need just one single handler to take care of all child nodes and that will save us a lot of memory space and processing time.

One handler to rule them all!

let rings = ['elves', 'dwarves', 'men'];

 

let ruleThemAll = (evt) => { 

  let race = evt.target.textContent;

  alert(`You rule ${race} \nI rule them all`);

};

 

let oneRing = document.querySelector('#oneRing'); 

oneRing.addEventListener('click', ruleThemAll);

 

let drawRings = (rings) => { 

  rings.forEach((ring) => {

    let ringEl = document.createElement('li');

    ringEl.innerText = ring;

    oneRing.appendChild(ringEl);

  });

};

 

drawRings(rings); 

See live demo

Now we are going somewhere. Our handler is bound once (and only once) to the parent <ul> element, and will take care of all its children. As you can see in the example above, it works perfectly well with dynamic content so you won't need to worry about that either.

So should I group all my handlers at the document level?

No. Not at all.

Let's say you go that way and listen to the click event on the document. The first problem is a gigantic function that will need to compare every possible target to redirect to the correct handler. That means multiple accesses and comparisons that are also costly, and depending on their amount, it can mean an actual decrease in performance rather than improving it.

That by itself is already a no-go argument but there are a couple of items on this equation to be considered: Scalability and Maintainability. Both of them tend to be worse using a document-level approach, since a monolithic structure is less flexible to begin with, and tends to grow even more inflexible.

The best solution is always to delegate them to the most meaningful outer container so you can gain the most from bubbling without worrying about whether that's the one you want to execute.

Wrapping Up

As we can see, event delegation not only promotes an efficient way to improve the speed of your web application, but also makes code more elegant by abstracting event-binding from the element creation level. The implementation itself is very simple as it's just a matter of moving the handler up on the DOM tree. Keep that in mind next time you use Javascript events :).

Happy coding!

 


Author

Rafael Guedes

Javascript enthusiast helping make a better web code by code. Front-end engineer on Avenue Code. Basketball lover and photographer wannabe.


A JAVA Developer's Perspective on Node.js

READ MORE

Node.js - Anti-patterns

READ MORE