How to throttle scroll and resize events in JavaScript

How to throttle scroll and resize events in JavaScript

The excitement of hooking into browser events can quickly sour when you realize just how frequently these events fire. Scroll events, mouse moves, window resizing – they can cascade down your script like a merciless flickering flood of calls, clobbering performance and turning smooth interactions into jittery chaos.

Take a simpler example: you are tracking mouse movement to update some element’s position or gather analytics data. With every pixel shift triggering a handler, your function gets called hundreds if not thousands of times per second. This, in turn, can cause massive CPU consumption, frame drops, and UI lag – the precise opposite of a good user experience.

Browsers only have so many CPU cycles to spare between rendering frames. JavaScript execution competes directly with layout, paint, and compositing processes. Unthrottled handlers hog the main thread, causing paint delays that manifest as stutter or unresponsiveness. It’s not just an annoyance; it’s a bottleneck that can cripple otherwise simple interactions on lower-end devices.

Even if your function is trivial, say a console log or a DOM update, the sheer frequency compounds the cost. And when side effects multiply, such as recalculating document layout or triggering reflows, the performance hit piles up exponentially.

Throw in complex logic, API calls, or animation frame requests, and the unthrottled event isn’t just inefficient – it’s a ticking time bomb. The common knee-jerk reaction might be to debounce instead, but that’s a different animal: debounce waits for the event storm to stop before executing, ignoring intermediate signals. Sometimes you want to respond continuously but not at full throttle.

That is where throttling shines. It’s the disciplined gatekeeper that allows your function to run at manageable intervals, maintaining responsiveness without flooding the engine with redundant calls.

Without throttling, you’re basically inviting a denial-of-service attack on your own application’s smoothness. Users feel it in tangible ways: jerky scrolls, input lag, sluggish UI elements. The technical debt piles up, and debugging becomes a nightmare of chasing ephemeral performance spikes tied directly to these excessively frequent events.

implementing a simple and effective throttling function

Implementing a throttling function is simpler and highly effective. The basic idea is to track the last time your handler ran and only allow it to execute again after a specified delay. This delay acts as a gatekeeper interval, ensuring your function can’t fire too often.

Here is a simple throttling function that returns a wrapped version of any given callback. It enforces a delay between invocations, ignoring calls that happen too soon after the last one:

function throttle(func, delay) {
  let lastCall = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func.apply(this, args);
    }
  };
}

Usage is as easy as wrapping your event handler before assigning it:

window.addEventListener('scroll', throttle(function(event) {
  console.log('Scroll event fired at', Date.now());
}, 200));

This example executes the scroll handler at most once every 200 milliseconds, no matter how wildly the user scrolls. That’s enough granularity to feel responsive while preventing a CPU overload from hundreds of calls every second.

This pattern preserves the original this context and forwards all arguments correctly, which is essential for handlers that rely on event objects or the component context in frameworks.

Sometimes you want a version that runs on the leading edge, triggering immediately, then ignores further calls until the delay passes. The above approach covers that. An alternative is a trailing edge throttle, which schedules a call after the delay even if events continue to come in. You can combine leading and trailing edge behavior with a more complex implementation if needed.

For completeness, here’s a more flexible version that supports leading and trailing calls with a cancel method to control the timer:

function throttleAdvanced(func, delay, options = {}) {
  let timeoutId = null;
  let lastCall = 0;
  let lastArgs = null;
  let lastThis = null;

  const { leading = true, trailing = true } = options;

  function invoke() {
    lastCall = Date.now();
    func.apply(lastThis, lastArgs);
    timeoutId = null;
    lastArgs = null;
    lastThis = null;
  }

  function throttled(...args) {
    const now = Date.now();
    if (!lastCall && !leading) lastCall = now;

    const remaining = delay - (now - lastCall);
    lastArgs = args;
    lastThis = this;

    if (remaining  delay) {
      if (timeoutId) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
      invoke();
    } else if (!timeoutId && trailing) {
      timeoutId = setTimeout(invoke, remaining);
    }
  }

  throttled.cancel = function() {
    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
    lastCall = 0;
    lastArgs = null;
    lastThis = null;
  };

  return throttled;
}

Applying this gives you precise control over when the throttled function fires, allowing for smoother integrations in complex event handling scenarios:

const optimizedResize = throttleAdvanced(() => {
  console.log('Window resized at', Date.now());
}, 250, { leading: true, trailing: true });

window.addEventListener('resize', optimizedResize);

Source: https://www.jsfaq.com/how-to-throttle-scroll-and-resize-events-in-javascript/


You might also like this video

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply