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

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

Open
FND opened this issue Jul 20, 2024 · 12 comments
Open

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

FND opened this issue Jul 20, 2024 · 12 comments
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: events

Comments

@FND
Copy link

FND commented Jul 20, 2024

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:

  • boolean: debounce: true and throttle: true would leave it up to browsers to decide a suitable delay, which might have performance and other contextual benefits (cf. requestAnimationFrame). While such a default will often be just fine, sometimes developers will need more control.
  • number: debounce: 200 and throttle: 200 could specify a delay in milliseconds, much like with setTimeout, most likely addressing 90+ % of all use cases.
  • Complex configuration options (e.g. lead time) exceed the scope of this proposal; see above. Such things could be implemented in userland while still benefiting from standardized rate limiting.
  • Activating both debouncing and throttling at the same time might result in confusion, so we might need to guard against that? (I'm not sure using both ever makes sense.)

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?

@FND FND added addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest labels Jul 20, 2024
@domenic domenic transferred this issue from whatwg/html Jul 20, 2024
@tanepiper
Copy link

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
Copy link

boutell commented Jul 22, 2024

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
Copy link

WebReflection commented Jul 22, 2024

@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
Copy link

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
Copy link

boutell commented Jul 29, 2024

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
Copy link

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
Copy link

bkardell commented Aug 5, 2024

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
Copy link
Author

FND commented Aug 6, 2024

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
Copy link

rezof commented Aug 6, 2024

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
Copy link

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

@keithamus
Copy link

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.

@FND
Copy link
Author

FND commented Oct 3, 2024

@keithamus et al.: I don't disagree, but the original proposal quite intentionally limited its scope to the event context - in part because that seemed like a potential quick win1, but also for ergonomic reasons: Even if debouncing and throttling were available in some other form (via observables2, as stand-alone functions etc.), exposing such functionality via corresponding addEventListener options would still add considerable value IMHO. (Note that OP's rationale also included broader aspects like educational benefits and potential context-specific optimizations.)

Footnotes

  1. Unfortunately, the enthusiasm here and elsewhere has yet to translate into interest by implementers though. Despite various efforts, it remains unclear how to get feedback from browser vendors.

  2. FWIW, both debouncing and throttling had already been mentioned in future methods on iterator helpers WICG/observable#35.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements needs implementer interest Moving the issue forward requires implementers to express interest topic: events
Development

No branches or pull requests

10 participants