Add event throttling and debouncing to AddEventListenerOptions


#1

The addEventListener method now accepts an optional, third AddEventListenerOptions argument 1. I propose adding options for event throttling and debouncing.

Example use case: Network Information API 3

navigator.connection.addEventListener('change', () => {
   // inspect navigator.connection
}, { debounce: true });

The change event fires multiple times in rapid succession, e.g., when the user’s connection switches from Wi-Fi to cellular 2, so it would be useful if a web app could specify that the event handler should be called only once after the final event (a.k.a. event debouncing).

If you have other examples of when event throttling or debouncing is needed, please comment below.


Update: Based on the discussion (as of November 20), I think it makes sense to treat debouncing and throttling as separate proposals, since their use cases are different.

Use cases for event debouncing:

  1. Debounce the Network Information API change event, since only the last event is relevant when multiple events fire in rapid succession.
  2. Debounce keyboard events when the user is typing into a form field, to be able to perform an action (e.g. load search results) when the user stops typing.
  3. Debounce the global resize event when the user resizes the browser window1. This event fires in rapid succession during a single user operation (demo), so a web app may want to receive a single event after the user has stopped resizing the browser window, in order to perform “UI changes that aren’t easily achievable with media queries.”

Use cases for event throttling:

  1. Throttle mouse events to a lower frequency (e.g., 20 Hz) when displaying mouse coordinates in the app’s UI to conserve processing power, since users can’t perceive UI updates at a higher frequency anyway.

Footnotes:

  1. There is a new Resize Observer API, which can notify the web app when an element’s size changes via resize entries. However, the same problem of multiple entries firing in rapid succession exists, so this new API does not remove the need for debouncing resize events in the use case described above.

#2

Debounce typically needs a ms delay specified

navigator.connection.addEventListener('change', () => {
   // inspect navigator.connection
}, { debounce: 1000 });

you’d also need options to specify leading or falling edge of the delay, not sure how you’d model that.


#3

Personally, I’d be happy with the browser deciding what the the debounce delay should be. The browser knows in which scenarios it fires events in rapid succession, so it knows best when to and when not to perform event debouncing. That seems like the most reliable option.

Regarding those additional values, I’m not sure what the use cases are for those.


#4

There are many ways to configure debouncing. It’s not just a delay period, which could be anything from 10ms to 60sec+. In some cases you want an immediate update on the first event and then a delay until the next event, and in others it’s better to have no event until a timeout expires.

It’s straightforward to write a throttling class with your own policy and pipe your event callback in to that, so I’m not sure this needs to be part of the spec.


#5

In some cases you want an immediate update on the first event and then a delay until the next event, and in others it’s better to have no event until a timeout expires.

It would probably make sense to provide a specific example of those other cases. Though anyway, this is solvable with another boolean option for addEventListener().


#6

Let’s focus on the use case. I’m not proposing to add a general-purpose event throttling and debouncing API to the web platform. I’m proposing a simple solution to a specific problem:

In certain situations, the browser fires events in rapid succession (for a valid reason). It would be useful if the web app could handle a single, debounced event in these cases, without needing to write custom code or use a library, would it not?

Look at my example. Notice how there are 6 events in rapid succession when the user switches from Wi-Fi to cellular. Only the last event matters, so event debouncing is not really optional—it’s mandatory: if I want to use the Network Information API, I have to pull in a library for debouncing. I just want the browser to give me the final event, and a simple debounce option could enable that.


#7

Throttling/debouncing is crucial for performance (e.g. for scroll event) and certainly deserves to be standardized. I believe it should be possible for web developer to specify a specific delay in each specific case.


#8

It would be useful, yes, but this is still vaguely defined. Couldn’t pointermove events be fired in rapid succession? How would that be debounced?

In Construct 3 we use debouncing (aka rate limiting) a lot - here are some of the places we use it:

  • throttling the rate of UI updates as parallel network requests complete
  • throttling the rate of DOM updates as state changes to avoid unnecessary performance overhead, e.g. reducing mouse position updates in the UI to 20 Hz, or limiting the rate of updating search results as the user types
  • throttling the rate of writing to storage as in-memory data changes
  • eliminating needless overhead of multiple simultaneous state changes by postponing updates for them until a timer fires, e.g. deleting multiple objects, each of which calls a Refresh() method, but which only really needs to be done once at the end
  • imposing limits on the rate custom events are fired, so listeners themselves don’t need to debounce

In some cases we want the first event to fire immediately and delay the next one (e.g. updating the mouse position, so it feels more responsive to input), but in others we prefer to not fire immediately and only run after a delay from the first event (e.g. updating the UI after a batch delete operation).

In fact I think the most significant point is rate limiters have wide application outside of event listeners. So it is more useful to implement them separately rather than hard-wiring them in to the event methods. In which case, using a library is a lot more flexible than trying to enshrine this in a permanent spec.


#9

If there’s a use case for debouncing pointermove events, it would be great if someone could describe it on an example. As I’ve said in my original post, I’m looking for more use cases, since one is not enough to motivate vendors.

Regarding network requests and memory storage, I don’t recall there being an event that you could observe (and potentially debounce).

Regarding custom events, I’m not sure that debouncing should apply to them. AFAIK, the whole point of a custom event is that it’s tailored to the app’s needs. The object that dispatches custom events should fire them at a rate that suits the app. Otherwise, that objects needs to be updated to dispatch events (maybe of a different type?) at the rate the app wants them.

Debouncing keyboard events as the user types into a form field is a good example. This could be the second use case. Once we have a bunch of use cases, we will have a clearer picture of what the default delay for debouncing should be (and if there should be a default at all).


#10

Precisely, which is why I advocate a library solution.

I gave one in my post: throttling mouse movement updates to 20 Hz. Modern browsers will issue these at 50Hz or more, and on the basis that 50ms is the threshold of perceiving an event as instantaneous, updates more frequent than 20 Hz are probably a waste of processing power.


#11

Well, if there are no events, then that’s unrelated to my proposal. :sweat_smile:

Could you give an example of why an app would need to throttle mouse events (what does the app update)? That would be the use case.


#12

I think a good usecase here isn’t apps generating their own events but frameworks generating events, an app may not need events at the rate a framework would issue but the alternative is for all libraries to bake their own methods in for debouncing or controlling event issuing. And from a developer point of view it’s added burden needing to treat custom events and built-in events differently.


#13

My point was that using a library covers more use cases than limiting this features to event listeners only. So it is better to use a library.

Our app displays the mouse position in a status bar. The rate at which the status bar updates the mouse position is throttled.


#14

Libraries are and will always be more feature-rich than the standard APIs, but that doesn’t mean that the standard APIs shouldn’t be (carefully) extended with features, when it makes sense. To me, “use a library” is not a solution.

This is a good use case for event throttling. I think we should split up the proposals for debouncing and throttling, since their use cases don’t seem related.


#15

Perhaps, but since rate-limiting has wide application outside of event listeners, large web apps will need both anyway. This causes the same feature to be duplicated, unnecessarily increasing complexity. Alternatively a large web app would simply ignore the standardised feature in favour of consistently using the library everywhere.

To solve this more elegantly, if you really think this belongs in the spec, I think a better idea would be to specify a debouncing/rate-limiting API in JavaScript, and then provide an easy way to link that to an event listener. For example, using independently:

function DoRealWork() { ... };
const rateLimiter = new RateLimiter(50 /* every 50ms, i.e. 20 Hz */, DoRealWork);
rateLimiter.Call(); // calls DoRealWork()
rateLimiter.Call(); // too soon, sets timer for next call

Or passing to an event listener:

const rateLimiter = new RateLimiter(50 /* ms */);

// passing a rateLimiter option causes the event listener callback to be throttled
window.addEventListener("pointermove", HandlePointerMode, { rateLimiter });

The RateLimiter class can be used for all use-cases: debouncing network info updates, throttling search updates while typing, throttling storage writes, throttling UI updates in mouse move events, etc.


#16

Obviously, not just mouse events, but also touch / pointer events (again to conserve resources/CPU on non-critical interactions on potentially low-powered devices) https://patrickhlauke.github.io/getting-touchy-presentation/#175


#17

Another use case would be to debounce the resize event. While resizing a browser window, you’re already hammering the CPU with reflows. Any operations that depend on a new window size should only be executed once the resizing is done, as was the case on older OS-es where only the window outline was repainted.

When thinking about frameworks and async updates, having a native way to throttle custom events for operation coalescing would also be beneficial.


#18

@AshleyScirra In order for such a rate-limiting API to be standardized, someone would need to describe the proposal and compile use cases for it, and that necessitates creating a new discussion here on WICG or somewhere else. I would welcome that, because it would allow me compare those use cases with the ones I’m compiling for the proposal in this discussion.


Could you give an example of such a resize-linked operation, i.e. what does the app need to perform after the user resizes their window? I’m trying to determine if there are alternative approaches that don’t require debouncing the resize event.


#19
  • Fetch new lazy-loaded resources if neccesary, eg new content or larger image resolutions.
  • Resize the canvas element to fit the new window dimensions.
  • Calculate new document and element dimensions for scroll-position based operations.
  • Trigger UI changes that aren’t easily achievable with media queries, eg collapsing sidebars.
  • Lazy loading and initializing new JS. For example, a map becomes visible at a certain breakpoint. At that time, the browser lazy-loads the required bundle and the gmaps api.

Basically all loading / parsing / executing should be postponed until the reflow storm finishes.


Add Event Handler metering, parallelism & throttling options to AddEventListenerOptions
#20

First, let me state that I think these are really useful options for event handling and I think we should try and get this to the WHATWG discussion mailing list (if not already done).

Second, reading through the discussion, I still think some additional behavior is warranted that could improve the setup of event handler behavior. This behavior could be considered orthogonal to this proposal though.

TLDR version: allow addEventListener to configure limits on parallelism of handlers (I call that metering by event topic) and queuing of incoming requests when the JS engine must make sure to handle other types of events.

I’ve written this up in more detail here (incl a reference to this proposal): Add Event Handler metering, parallelism & throttling options to AddEventListenerOptions