Capturing JavaScript events such as a window resize or document scroll can result in dozens of events fired in succession for a single user action. It may be useful to limit this rate for expensive function calls or UI purposes such as animations or committing the value of a scripted text field after user input.
Here is a quick inline implementation of this rate limiting for event listeners. We use a closure to neatly encapsulate our event listener via an IIFE. 1
Let D_TIME
be the minimum interval in milliseconds between function calls.
Option 1: Debounce
This calls your function after D_TIME
has elapsed without the event firing. This method is known as
debouncing,
not to be confused with throttling. Excess calls are discarded rather than queued. Most UI-related cases will probably use this.
document.addEventListener("scroll", ((D_TIME = 250) => {
let timeout = 0;
return () => {
window.clearTimeout(timeout);
timeout = window.setTimeout(fn, D_TIME);
};
/** Your function */
function fn() {
console.count(fn.name);
}
})());
Option 2: Throttle
This calls your function immediately, only if D_TIME
has elapsed since last call. This is a method of throttling. Excess calls are still discarded.
document.addEventListener("scroll", ((D_TIME = 250) => {
let lastTime = 0;
return () => {
if (Date.now() - lastTime < D_TIME) return;
lastTime = Date.now();
fn();
};
/** Your function */
function fn() {
console.count(fn.name);
}
})());
Option 3 (Combined)
The above two combined, resulting in a steady series with an additional final call at the end:
document.addEventListener("scroll", ((D_TIME = 250) => {
let timeout = 0, lastTime = 0;
return () => {
window.clearTimeout(timeout);
timeout = window.setTimeout(fn, D_TIME);
if (Date.now() - lastTime < D_TIME) return;
lastTime = Date.now();
fn();
};
/** Your function */
function fn() {
console.count(fn.name);
}
})());
Alternatively, the same combined example using the parent scope instead of a closure: 1
const D_TIME = 250;
let timeout = 0;
let lastTime = 0;
document.addEventListener("scroll", () => {
window.clearTimeout(timeout);
timeout = window.setTimeout(fn, D_TIME);
if (Date.now() - lastTime < D_TIME) return;
lastTime = Date.now();
fn();
});
/** Your function */
function fn() {
console.count(fn.name);
}
Option 4: Queue
This is an entirely separate scenario from the above. I have again used a scroll listener, only to represent an arbitrary rapid series of events. Rather than discarding excess calls, every invocation is queued and processed in order at interval D_TIME
.
document.addEventListener("scroll", ((D_TIME = 250) => {
const queue = [];
let interval = setInterval(() => queue.shift()?.(), D_TIME);
return () => queue.push(fn);
/** Your function */
function fn() {
console.count(fn.name);
}
})());
You could length-limit queue
, clear/restart interval
as needed, or replace the interval timer with some other coroutine from your application.
Footnotes
-
I’m a huge fan of IIFE closures for creating an object-like scope with hoisted function “methods” for readability (and to avoid polluting global/module scopes). Since excessive indentation tends to hurt maintainability I generally only use them at one nesting level where a function reference is already needed. ↩ ↩2