Three small classes under utilities/Limiters/ cover the recurring problem of
doing something less often, or later. Debouncer and Throttler both share one
convention worth committing to memory: you trigger them with run(), not by
calling the instance.
Debouncer
Collapses a burst of triggers into a single invocation. Construct it with a
callback and delay, plus three booleans that pick which edges fire, then call
run() on every event:
new Debouncer(callback, delay = 100, leading = false, trailing = true, required = false)
| Option | Default | Fires… |
|---|---|---|
leading |
false |
immediately on the first trigger of a burst |
trailing |
true |
once the burst has been quiet for delay |
required |
false |
on any trigger where delay has already elapsed since the last run |
On each run() it applies leading first, then the required check, then (re)arms
the trailing timer — so a steady stream of triggers collapses to the edges you asked
for.
When you just want a debounced function rather than an instance to hold, the
static Debouncer.debounce(...) builds one and hands back its bound run:
// components/Masonry/Masonry.js — leading + trailing, 50ms
this.#_refresher = Debouncer.debounce(
() => this.#_refresh.call(this),
50,
true,
true
)
// call the returned function on every event; it debounces internally
this.#_refresher()
Throttler
Guarantees a minimum interval between runs — the callback fires at most once per
delay, and intervening triggers are dropped (no trailing catch-up). Same
trigger convention: run() on every event, and the same static shortcut for a
bound function:
const onScroll = Throttler.throttle(update, 100)
window.addEventListener('scroll', onScroll)
Debounce vs throttle. Debounce waits for the storm to pass and fires once at the end (trailing) and/or the start (leading). Throttle fires on a steady cadence throughout. Reach for throttle on a continuous stream you want to sample (scroll), debounce on a burst you want to settle (resize, input).
Sleep
A cancellable, awaitable delay. Sleep.for(ms) returns a thenable you can
await; calling .stop() resolves it immediately and cancels the pending
timer. That makes it a restartable timer, not just a setTimeout wrapper:
// modules/Gallery/CarouselGallery.js — a self-cancelling filter delay
this.#_filterSleep.stop() // cancel any in-flight wait
this.#_filterSleep = Sleep.for(250) // start a fresh one
// …elsewhere:
await this.#_filterSleep // resolves after 250ms, or the moment .stop() runs
Because it's thenable, it drops straight into async flow with no wrapping
promise — await Sleep.for(250) is all you write.
Seed a field with Sleep.for(0) (already-settled) so the first .stop() and
reassignment have something to act on — the pattern the Gallery and
VirtualTours carousels both use.
Related
- Global observers — Resize consumers commonly debounce their reactions.
- Scheduling — the other half of the theme's timing toolkit.