throttle
The throttle utility ensures that a function is called at most once in a specified time interval. It is perfect for limiting the execution rate of heavy handlers like scroll, mouse movement, or continuous API polling.
Implementation
View Source Code
ts
import type { Fn } from '../types';
import { assert } from './assert';
export type ThrottleOptions = {
leading?: boolean; // invoke at the start of the window
trailing?: boolean; // invoke at the end with the last args
};
export type Throttled<T extends Fn> = ((this: ThisParameterType<T>, ...args: Parameters<T>) => void) & {
cancel(): void;
flush(): ReturnType<T> | undefined;
pending(): boolean; // whether there's a pending call that flush() would execute
};
/**
* Throttles a function. By default, leading and trailing are both true (lodash-like behavior).
* The function is invoked at the leading edge and trailing edge of the throttle period.
*
* Example:
* const fn = () => ...
* const t = throttle(fn, 700);
* const leadingOnly = throttle(fn, 700, { trailing: false });
*/
export function throttle<T extends Fn>(
fn: T,
delay = 700,
options: ThrottleOptions = { leading: true, trailing: true },
): Throttled<T> {
assert(typeof fn === 'function', 'First argument must be a function', {
args: { fn },
type: TypeError,
});
assert(typeof delay === 'number' && delay >= 0, 'Delay must be a non-negative number', {
args: { delay },
type: TypeError,
});
const leading = options.leading ?? true;
const trailing = options.trailing ?? false;
let timer: ReturnType<typeof setTimeout> | undefined;
let lastInvokeTime = 0;
let lastArgs: Parameters<T> | undefined;
let lastThis: ThisParameterType<T> | undefined;
let lastResult: ReturnType<T> | undefined;
const clearTimer = () => {
if (timer !== undefined) {
clearTimeout(timer);
timer = undefined;
}
};
const invoke = (now: number) => {
lastInvokeTime = now;
clearTimer();
if (!lastArgs) return undefined;
const args = lastArgs;
const ctx = lastThis as ThisParameterType<T>;
lastArgs = undefined;
lastThis = undefined;
// biome-ignore lint/suspicious/noExplicitAny: -
lastResult = fn.apply(ctx as any, args);
return lastResult;
};
const remaining = (now: number) => delay - (now - lastInvokeTime);
const timerExpired = () => {
const now = Date.now();
if (lastArgs && remaining(now) <= 0) {
// trailing edge invoke
invoke(now);
} else if (lastArgs) {
// reschedule until a window elapses
timer = setTimeout(timerExpired, remaining(now));
} else {
clearTimer();
}
};
const throttled = function (this: ThisParameterType<T>, ...args: Parameters<T>) {
const now = Date.now();
if (lastInvokeTime === 0 && !leading) {
// If leading is false, start the window now but don't invoke immediately
lastInvokeTime = now;
}
lastArgs = args;
lastThis = this;
const rem = remaining(now);
if (rem <= 0) {
// Window elapsed: invoke now
invoke(now);
} else if (trailing && !timer) {
// Schedule trailing call if not already scheduled
timer = setTimeout(timerExpired, rem);
}
} as Throttled<T>;
throttled.cancel = () => {
clearTimer();
lastArgs = undefined;
lastThis = undefined;
lastInvokeTime = 0;
};
throttled.flush = () => {
if (!lastArgs) return undefined;
const now = Date.now();
return invoke(now) as ReturnType<T> | undefined;
};
// Pending if a trailing call is scheduled OR there are queued args.
throttled.pending = () => lastArgs !== undefined || timer !== undefined;
return throttled;
}Features
- Isomorphic: Works in both Browser and Node.js.
- Performance Optimized: Reduces CPU usage by dropping redundant intermediate calls.
- Type-safe: Preserves the argument types of the original function.
API
ts
type ThrottledFunction = {
(...args: any[]): void;
cancel: () => void;
flush: () => void;
};
function throttle<T extends (...args: any[]) => any>(fn: T, limit?: number): ThrottledFunction;Parameters
fn: The function you want to throttle.limit: The minimum time (in milliseconds) that must pass between successive calls tofn(defaults to700).
Returns
- A throttled function with two additional methods:
cancel(): Resets the throttle timer and cancels any pending execution.flush(): Immediately executes any pending call.
Examples
Scroll Event Handling
ts
import { throttle } from '@vielzeug/toolkit';
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
// Perform heavy calculation or UI update
}, 100);
window.addEventListener('scroll', handleScroll);Rate-limited Search
ts
import { throttle } from '@vielzeug/toolkit';
const rateLimitedSearch = throttle((query: string) => {
console.log('Fetching results for:', query);
}, 1000);
// Only one call will execute per second even if called faster
rateLimitedSearch('a');
rateLimitedSearch('ap');
rateLimitedSearch('app');Using Cancel and Flush
ts
import { throttle } from '@vielzeug/toolkit';
const trackEvent = throttle((event) => {
console.log('Tracking:', event);
}, 2000);
trackEvent('click'); // Executes immediately
trackEvent('click'); // Queued
trackEvent('click'); // Replaces queued
// Reset and cancel pending
trackEvent.cancel();
// Or execute pending immediately
trackEvent('scroll');
trackEvent.flush(); // Executes 'scroll' immediatelyImplementation Notes
- The throttled function does not return the result of the original
fn. - The first call to the throttled function executes immediately.
- Subsequent calls within the
limitperiod are ignored until the timer expires.