Svelte: Event delegation (e.g. for click listeners)

Created on 12 Jan 2018  Â·  6Comments  Â·  Source: sveltejs/svelte

I brought up in the Gitter today that it might be nice to have built-in support for event delegation. For instance, if I have a list of 1000 buttons, I don't necessarily want to add 1000 click listeners to each one – I can just add one to the parent and let the event bubble up. Presumably this has a perf benefit.

But of course... all perf benefits should be investigated and measured. So I whipped up a quick benchmark to test this out.

This benchmark creates a list of _n_ buttons and then measures the time to add the necessary event listeners, with and without delegation. It also allows you to delay the creation of event listeners to better suss out memory usage, and it prints out the response time using performance.now() - event.timeStamp (note: not usable in IE/Safari due to lacking high-precision timers for event.timeStamp). Also note that it only ever creates one function, so it's not measuring the cost of creating multiple functions.

There are 3 main perf aspects we're interested in:

  1. Time to set up the event listeners themselves
  2. Memory usage
  3. Time it takes for the event to bubble to the parent vs just listening on the element itself.

1. Time to set up the event listeners themselves

For this, I tested on an i7 ThinkPad in all browsers except Safari, which I tested on an i5 MacBook Air. Times reported are in milliseconds, median of 5 runs. Note that Safari throttles high-precision timings to 1ms.

Browser | 100 elements | 1000 elements | 10000 elements | 100 elements w/ delegation | 1000 elements w/ delegation | 10000 elements w/ delegation |
|---|---|---|---|---|---|---|
|Edge 16 | 0.50 | 4.06 | 11.84 | 0.04 | 0.04 | 0.02 |
|Chrome 63 | 0.46 | 2.61 | 31.95 | 0.04 | 0.04 | 0.04 |
|Firefox 57 | 0.86 | 4.00 | 31.94 | 0.04 | 0.04 | 0.04 |
|IE 11 | 1.34 | 11.58 | 31.34 | 0.08 | 0.10 | 0.04 |
|Safari 11.0.2 | 0.00 | 1.00 | 12.00 | 0.00 | 0.00 | 0.00 |

So clearly event delegation wins in all browsers pretty handily. This makes sense, because the browser is simply doing less work (i.e. only calling addEventListener() once as opposed to multiple times). The cost of non-delegation increases as the number of child nodes increases.

2. Memory usage

For analyzing memory, I used Windows Performance Analyzer with the "VirtualAlloc Commit LifeTimes" view, summing the results for each browser process. (Too much detail required to explain all the steps involved, but maybe it's worth a blog post. 😉) I ran the test with 1 run only and 10000 elements, and a delay of 10000ms to better isolate the memory costs.

Here, the results were a bit… odd. Edge and Firefox appear to use much less memory in the delegation scenario versus the non-delegation scenario, which makes sense – there's 1 listener instead of 10000. For Chrome, though, it seems to actually use _more_ memory when you delegate, which surprised me. Results:

  • Edge: 3.254MB with delegation, 6.570MB without (50.47% improvement)
  • Firefox: 2.383MB with delegation, 8.411MB without (71.67% improvement)
  • Chrome: 44.34MB with delegation, 32.043MB without (27.74% regression) _error: amended below_

I was really surprised by what I saw with Chrome, so I ran the test again and got a similar result. This may need more investigation. I also haven't figured out how to analyze memory in Safari.

3. Time it takes for the event to bubble

For this one, I took a cursory glance and observed that the response times seem to be roughly the same with and without event delegation. Maybe this is worth testing on a slower mobile device, or maybe it's worth testing with a very deep hierarchy, but I haven't gone that far yet.

Conclusion

So basically there is more work to do – I need to figure out if the Chrome memory behavior is genuine, and this probably needs to be tested on mobile devices to ensure bubbling doesn't have an exorbitantly high cost, but just based on these preliminary results it seems it's still useful to do event delegation, at least for click listeners.

perf proposal

Most helpful comment

Just to loop back on this: I solved this using a custom implementation, and I'm no longer sure it should be handled by the framework. I had to do some custom stuff for a11y, e.g. to handle both keydown on the Enter key as well as 'click' events. Not sure if Svelte could reasonably handle that kind of thing while satisfying every use case (and making it performant; e.g. not doing work for non-Enter keydown events).

Dead-simple example here: https://gist.github.com/nolanlawson/621081285dbbae5be97a2bdf7a6d7ce5

All 6 comments

Thanks so much for looking into this, it's very interesting. I suppose we would also need to consider the cost of doing this sort of thing inside the handler...

let node = event.target;
while (node && !node.matches(selector)) node = node.parentNode;

..., which may be non-trivial. (I suppose we probably wouldn't be using matches if this were to happen automatically — not sure exactly how it would work.) Then again the cost of doing excess work when an event happens is unlikely to outweigh the cost of the initial setup without delegation.

There was a proposal the other day to add event 'modifiers' to automatically stop propagation, prevent defaults etc. Maybe if we decided we wanted delegation but weren't sure about doing it automatically, we could do something crazy like this?

<ul on:click:delegate('li button')='doThing()'>

(That's probably an extremely bad idea.)

Yeah, I agree that the cost when the click handler actually runs (i.e. point # 3 above) is an important dimension to capture. Maybe in some cases the setup cost reductions aren't worth the responsiveness costs (so making it explicit rather than automatic makes sense to me).

BTW I figured out the source of the Chrome discrepancy: Chrome was allocating some memory temporarily and then freeing it after a few seconds, so I had to increase the wait time in order to let the memory reach a steady state. To give you an idea of what this looks like in Windows Performance Analyzer:

2018-01-11 17_39_20-warpwhistle 01-11-2018 17-33-26-chrome-10000-with-event-delegation-20000-delay e

The initial bump is from creating the <button> elements themselves, then I let it sit for 20 seconds, add the event listeners, and then let that sit for another minute. I also closed and reopened the browser for each test. (Measuring memory is hard. 😞)

In any case, my new numbers for the 10000 elements scenario are:

  • Chrome: 13.882MB with delegation, 24.82MB without delegation (44% improvement).

That makes a lot more sense; 1 listener should be cheaper than 10000. 😅

Rich-Harris's point is very important when it comes to event delegation at the library level if you intend to implement the same bubbling semantics that browsers do.

Another question to ask is if the setup involved to correctly reference the event handler with the related data/context when assigning all the events, i.e does addEventListener perform much slower and consume more memory than whatever booking you would need to setup and retrieve references.

Where the bench might setup 1 addEventListener reference for event delegation and 10000 addEventListener without, implementing this would look more along the lines of setting up 10000 references setup through addEventListener vs 1 addEventListener + 9999 references setup through the data-structure of choice used for book keeping.

Factoring these two points into the bench should theoretically scale the amount of work to be done to weight on the side of implementing this in JavaScript when you consider that browsers are likely at a better position to do these two tasks more efficiently, and in either scenario both should be expected to use linear time and space.

That's a fair point; I'm not sure exactly how much bookkeeping is required given Svelte's internal architecture. That too could impact the setup costs.

In any case, this isn't a hugely important feature – users can always work around it by implementing their own delegation or using something like a virtual list.

Just to loop back on this: I solved this using a custom implementation, and I'm no longer sure it should be handled by the framework. I had to do some custom stuff for a11y, e.g. to handle both keydown on the Enter key as well as 'click' events. Not sure if Svelte could reasonably handle that kind of thing while satisfying every use case (and making it performant; e.g. not doing work for non-Enter keydown events).

Dead-simple example here: https://gist.github.com/nolanlawson/621081285dbbae5be97a2bdf7a6d7ce5

Fixed benchmark: http://nin-jin.github.io/deleg/
There is no difference in real world. More over, direct event handlers attachment are bit more efficient.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AntoninBeaufort picture AntoninBeaufort  Â·  3Comments

ricardobeat picture ricardobeat  Â·  3Comments

thoughtspile picture thoughtspile  Â·  3Comments

plumpNation picture plumpNation  Â·  3Comments

mmjmanders picture mmjmanders  Â·  3Comments