Some JS events come in a continuous stream of updates: mousemove, resize, scroll. The user input is continuous, and the browser fires off update events as fast as it can.

But you often only want to update the page after the user has stopped fussing with the web page. This is especially important if you're going to be running a lot of code in reaction to the event (like re-drawing a visualization after resize).

Throttling events, or de-bouncing events, means to only react once to a series of updating events.

In the simplest case, that means waiting until the event stream stops before reacting to them. That creates a bit of a delay (as you wait to see if another event is coming), but it avoids any lags from running lots of JavaScript code that is just going to be invalidated a moment later.

Sounds complicated? Sort of. It only takes a few lines of code, but you need to wrap your head around it.

You need to:

  • set a timer as soon as the event comes in, to create the delay;
  • restart the delay timer if another matching event comes;
  • run the real listener code when the timer finally completes.

The setTimeout and clearTimeout global methods can take care of the delays. The following code would prevent the realListenerFunction from running until half a second (500ms) had passed after the final event in a series:

  var timeout;
let throttledListener = function() {
  if (timeout) clearTimeout(timeout); // cancel the old timer
  timeout = setTimeout(realListenerFunction, 500);
}

element.addEventListener(eventType, throttledListener);

But there's a problem with this approach: we're using a global variable to keep track of whether there's a timer running from a previous event. This could get messy if you have lots of code and events.

For each event handler, you want to keep track of the timeouts separately.

The solution is to use the generator function pattern: a function that you call immediately, which returns a new function that will actually listen for events.

(Note: this is not the same as the generators and GeneratorFunction defined in ES6, which are a much more complicated way of creating a function that preserves state across multiple function calls.)

In this code, throttleEvents is our generator function, we call it with our real listener function (the one that will actually react to the event) and our desired delay time to create the throttled listener function:

  function throttleEvents(listener, delay) {
    var timeout;
    var throttledListener = function(e) {
        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(listener, delay, e);
    }
    return throttledListener;
}
element.addEventListener(eventType, 
            throttleEvents(realListenerFunction, 500) );

Every time you call throttleEvents, a new variable scope is initialized, with a new copy of any variables declared inside: a new timeout variable and a new throttledListener function. That inner function is returned when we call the generator, and its the function that actually listens for events.

(It works with anonymous returned functions too, which is what I use in the pens. But in order to talk about the code it helps to have a name.)

The returned listener function (aka throttledListener) has access to the variables from its parent scope (aka, that particular run of throttleEvents), including the timeout variable. The timeout therefore persists after the generator function returns, and across function calls of the returned listener.

So for that particular generated listener function, the timeout variable acts kind-of like a global. But if you generate a separate listener function (by running throttleEvents again, possibly with different parameters), it would have a separate variable keeping track of its own timeouts. And none of those timeout variables are cluttering up your global scope.

See it in action here:

David Corbacho wrote a much more detailed post on de-bouncing and throttling for CSS-Tricks. By his definition, what I'm doing in the previous pen is de-bouncing. Throttling would be to react to the initial event right away, then to wait a minimum delay before reacting to another similar event.

That would look like this:

  function actThenThrottleEvents(listener, delay) {
  var timeout;
  return function(e) {
    if (!timeout) { // no timer running
      listener(e); // run the function
      timeout = setTimeout( function() { timeout = null }, 
        delay); // start a timer that turns itself off when it's done
    }  
    //else, do nothing (we're in a throttling stage)
  }
}

The following pen uses both an act-then-throttle listener and a throttle-then-act listener. They create separate event listeners for the start and end of a scroll action:


8,505 4 80