Add event throttling and debouncing to AddEventListenerOptions


#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


#21

These proposals fail to think about what happens when multiple event listeners are added and there are handlers that don’t throttle/debounce the events. What is the dispatching model?

If certain code requires throttled events and other code doesn’t you want the UA to keep track of each event listener state? That is a complex model to store.

In the case of pointermoves Chrome “throttles” these to match the display frequency. In fact they are sent just before rAF time so that we eliminate unnecessary work.


#22

I don’t know how browsers implement this, but my guess is that they would need to postpone invoking handlers that have the debounce option until after the event has stopped firing (in my proposal, the browser would decide what that interval is).

I don’t know how complicated this is to implement. My job is to compile use cases (see my original post above for the list), and then the browsers can decide if it’s worth implementing this in order to free developers from having to use libraries for event debouncing.

See the use case for throttling in my original post, where I give an example of when throttling to 60 FPS is not enough.


#23

This problem is also solved by using a JS class. You can just add an event listener like this:

document.addEventListener("pointermove", e => rateLimiter.Call(e));

Then the browser doesn’t need to worry about any of the complexity of how to dispatch the events.


#24

I am fairly certain that browsers can implement event debouncing just fine, so we needn‘t worry about them. What we do need to worry about are standard APIs that aren‘t sufficient for creating performant web apps.

Case in point: Eric Bidelman has shared a component that fires resize events when it’s resized. His tweet has been retweeted and liked hundreds of times. Developers certainly seem to like this type of feature. However, the component taxes my fairly powerful CPU. This is unacceptable. We need to fix this by enhancing the standard APIs, so that they can be performant by default.

This is not about the individual preferences of your (or anyone else’s) large app. This is about the web platform. It needs event debouncing, so that we can have performant apps by default. I have compiled a list of 3 use cases for event debouncing in my original post. I think, that’s enough to start discussing a potential API. Specifically, should the debounce option be a Boolean or number?


#25

What about if you want to change the debouncing options, such as to disable it, or alter the timeouts in response to user settings or activity? With the event listener API is it intended that the event has to be entirely removed and re-added with new settings? That will have other subtle side-effects like possibly re-ordering its firing sequence relative to other listeners, as well as requiring re-specifying all other event listener options at the same time (passive, capture etc).

Performant by default is of course a worthy goal, but it seems to solve a lot of problems, apply to more cases (e.g. pure JS events), and make it more flexible and future-proof if you move this in to JS land. We could look to standardise a JS class to do this, but then by that point, why not just write a library? Specs are great, but once v1 ships they become difficult to change, and in the long term can end up as a backwards-compatibility headache. Libraries avoid all of that. So in my opinion, generally anything that can be a library, should be.


#26

An event handler is bound to its debouncing option. I’m not sure for what reason an app would need to change this subsequently.

Note that the proposal is a JS API, so we’re already in JS land. I think, I’ve already answered your question. The web platform needs to have APIs that are performant by default, without libraries. In the use cases I’ve described in my original post, the standard APIs cause performance issues. Currently, you just cannot handle certain events without wasting the user’s CPU.