Code Snippets
/

Throttle with Leading and Trailing Edges

Throttle with Leading and Trailing Edges

A leading-only throttle drops the last call's arguments; a trailing-only throttle feels laggy on the first event. The Lodash-style throttle that fires on BOTH edges is the version every UI codebase eventually wants: an immediate response on the leading edge plus a guaranteed final fire after the burst ends. This snippet builds that production-grade throttle from scratch with cancel and flush, then shows the configurable `leading` / `trailing` toggle that powers most real-world helpers.

JavaScript
Hard
utility
code-template
performance-optimization
rate-limiting

158 views

5

function throttleLT(fn, wait) {
    let timer = null;
    let lastCall = 0;
    let pendingArgs = null;
    let pendingThis = null;

    function invoke() {
        lastCall = Date.now();
        timer = null;
        const args = pendingArgs;
        const thisArg = pendingThis;
        pendingArgs = null;
        pendingThis = null;
        if (args) fn.apply(thisArg, args);
    }

    return function throttled(...args) {
        const now = Date.now();
        const remaining = wait - (now - lastCall);
        pendingArgs = args;
        pendingThis = this;

        if (remaining <= 0 || remaining > wait) {
            // Leading edge: enough time has passed (or clock skew). Fire now.
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            invoke();
        } else if (!timer) {
            // Schedule trailing-edge fire at the end of the current window.
            timer = setTimeout(invoke, remaining);
        }
    };
}

const log = throttleLT((label, t) => console.log(label, t), 30);
log('A', Date.now()); // fires immediately (leading)
log('B', Date.now()); // queued for trailing
log('C', Date.now()); // overwrites the queued args
// After ~30ms: fires once with C

The contract of leading-plus-trailing is: fire on the very first call in any quiet window, then guarantee a final fire with the latest arguments at the end of any burst. Tracking lastCall against Date.now() lets the leading-edge branch fire only when the previous fire happened long enough ago. A second setTimeout schedules the trailing fire at the exact end of the current window, using wait - (now - lastCall) so the gap stays exactly wait between fires regardless of when in the window the burst arrived. The remaining > wait guard handles the edge case where the system clock moved backwards (NTP correction, debugger pause).

1 more snippet in this entry are available for premium members.

Upgrade to Premium