whatwg / dom

DOM Standard
https://dom.spec.whatwg.org/
Other
1.56k stars 289 forks source link

Proposal: rate limiting event listeners (debounce / throttle) #1298

Open FND opened 1 month ago

FND commented 1 month ago

What problem are you trying to solve?

Developers often need to limit the frequency with which event handlers are being executed, both to improve performance and for behavioral reasons. Typically there are two distinct purposes, particularly when dealing with browser events fired in rapid succession: Debouncing delays the respective action until a steady state has been reached while throttling limits execution to once per time frame. (For details, see Debouncing and Throttling Explained Through Examples, perhaps also consult another visualization.)

Examples include avoiding excessive GUI updates, either for performance reasons (e.g. in response to resizing or scrolling) or to prevent flickering (e.g. when visualizing pointer coordinates or network-connection states), as well as reducing the frequency of network requests (e.g. auto-completion for keyboard inputs). A proposal from 2017 by @simevidas discusses additional use cases and considerations.

Providing a standardized approach for this common operation would enhance the web platform by reducing the need for custom implementations or third-party dependencies (in fact, it's not uncommon for an application to include multiple such implementations). In addition to browsers offering a reliable and efficient implementation, adding this capability to the platform would likely provide educational benefits by raising general awareness of debouncing and throttling.

What solutions exist today?

There are myriad JavaScript implementations for both debouncing and throttling, going back to at least 2009 with John Hann and Ben Alman. The concepts are also widely used in reactive programming; there might be parallel efforts within the context of the recent JavaScript proposals for observables and signals.

Presumably browser internals already include this functionality, without it being exposed to web developers.

How would you solve it?

Here we're primarily trying to address the issue of controlling browser events fired in rapid succession. Thus the obvious solution seems extending addEventListener with two new options: debounce and throttle (complementing once).

document.body.addEventListener("input", console.log, {
    debounce: 200
});
document.body.addEventListener("pointermove", console.log, {
    throttle: true
});

Note that a general-purpose solution for debouncing and throttling in JavaScript would exceed the scope of this particular proposal. (Though it's conceivable that might happen naturally in the future, if perhaps more through convention than via shared implementations.)

That leaves the question which values those options should assume:

My inclination is that supporting both boolean and numbers would be nice, but I'm not sure there's a precedent for that. It might also complicate documentation and education.

Anything else?

While I had considered creating a speculative polyfill, a future-proof implementation requires feature detection - that seems tricky for addEventListener options? In fact, support detection might need additional consideration before any such feature is introduced.

I've belatedly realized #1070 already exists (thanks to domenic's issue transfer); is there a process for merging both issues?

tanepiper commented 1 month ago

I think this is exactly one of those cases I've found over the last 10-15 years that always has to be implemented somewhere in a UI, but requires a library or additional code to do.

+1 for non-breaking additional properties that would add this support at the platform level for denouncing.

Throttle and denounce are not the same IMHO so both could be valid. Denounce is the event handler on last event, but you could still slow down a UI without throttling.

boutell commented 1 month ago

I find that this is most relevant when the handler itself will take time to execute, a variable amount of time, and there would be negative consequences of simultaneous execution of several instances of that asynchronous handler, e.g. an autocomplete input not ultimately displaying the final set of suggestions due to a race condition.

As such, I think that the usefulness of this feature would be greatly increased if it detected promises returned by the handler and ensured that they reached resolution before allowing another invocation.

WebReflection commented 1 month ago

@boutell I feel like await: true would be a better/explicit guard to that and it can be used to already queue same async listener multiple times ... it's indeed a common footgun not always understood by developers that multiple dispatches to an async listener doesn't mean that the previous same listener completed whatever it was doing and it easily cause broken states on the UI if the latest dispatch finished before the previous one.

edit this might have undesired side-effects around the event.currentTarget and other properties gone by the time the latest async listener runs ... but that's why await: true looks like an awesome guard beside debounce and throttle to solve even more common use cases.

bkardell commented 1 month ago

See also webwewant.fyi/wants/4/ - judges pick and we did try to move that at some point iirc - I can't remember why it stalled

boutell commented 1 month ago

I don't have any strong objection to an explicit option to turn on the behavior of always settling the previous promise before starting another invocation, although that this is a new API to start with so it could be reasonable to define its relationship to promises right from the start. If there's a super common case where this would be undesirable behavior I'd be interested to hear more about it.

paulshryock commented 1 month ago

I think I'd prefer throttle and debounce only accept a number. If they're not present, then they're off.

Either that, OR, if there needs to be a default duration, then:

Rather than allowing throttle and debounce to accept both booleans and numbers, I'd rather see separate fields for the booleans and numbers.

Something like:

{
  debounce: true,
  debounceDuration: 200,
  throttle: true,
  throttleDuration: 200,
}

And the *Duration fields would be optional. The duration would fall back to the default duration if those were not specified.

bkardell commented 1 month ago

Would it be better to make it so that you can't set both? Something like...

somethingBikesheddable: { type: 'throttle', duration: 200 }

Where I am not even proposing a name for this, but just a way to structure the arguments such that you don't both debounce and throttle?

FND commented 1 month ago

I like the thinking here, though we might also want to consider whether there's a precedent or whether we'd be introducing new API patterns (which might be warranted, but should be a conscious decision). Right now, I'm struggling to think of anything comparable within existing APIs, though that doesn't necessarily mean much...

rezof commented 1 month ago

I'd prefer if these functions (throttle/debounce) were built-in to javascript. Limiting the eventListener call rate is useful but it's also limiting.

Let's say for example i have two actions to perform when an event is triggered, one of them requires throttling and the other doesn't, that will force me to create 2 event listeners.

zaygraveyard commented 1 month ago

+1 for built-in throttle and debounce functions independently from this proposal.

keithamus commented 1 month ago

It may be worth proposing these extensions as part of the Observables proposal which has the ability to add prototype methods, has a contract of variable timing (sync or async - therefore has good precedence for such operations), and is not strictly coupled to Events.