Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event delegation (e.g. for click listeners) #1098

Closed
nolanlawson opened this issue Jan 11, 2018 · 10 comments
Closed

Event delegation (e.g. for click listeners) #1098

nolanlawson opened this issue Jan 11, 2018 · 10 comments
Labels

Comments

@nolanlawson
Copy link
Contributor

nolanlawson commented Jan 11, 2018

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.

@Rich-Harris
Copy link
Member

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.)

@nolanlawson
Copy link
Contributor Author

nolanlawson commented Jan 12, 2018

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. 😅

@thysultan
Copy link

thysultan commented Jan 12, 2018

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.

@nolanlawson
Copy link
Contributor Author

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.

@nolanlawson
Copy link
Contributor Author

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

@nin-jin
Copy link

nin-jin commented Dec 29, 2019

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.

@GermanJablo
Copy link

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.

@nin-jin Do you still have the benchmark code? The results don't match this: krausest/js-framework-benchmark#561 (comment)

@nin-jin
Copy link

nin-jin commented Feb 11, 2024

I no longer remember what the problem was with the original benchmark. This code is currently unavailable. But I think a typical mistake was made there: not all browser operation was measured, but only the synchronous call of some apis. This gives false confidence about the importance of optimization. It's like comparing icebergs by the size of their small surface area.

@nolanlawson
Copy link
Contributor Author

@nin-jin I think the benchmark link 404s because bl.ocks.org shut down. But you can still access the original gist: https://gist.github.com/nolanlawson/ea3ea9899d3a7f26579c1ff374f8d67b/

@nin-jin
Copy link

nin-jin commented Feb 17, 2024

Yes, as I said: compared to the cost of creating a DOM, the cost of adding handlers is not noticeable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants