Throttle Function in JavaScript

Continuing our interview coding questions series, today we'll look at how we can implement a throttle function in JavaScript. This function is similar to the debounce function that we looked at before. The do have some suttle differences that we'll discuss in a moment.

Both functions take a callback function and a delay parameter. For a debounce function, a callback will only be called once when the specified delay in milliseconds has elapsed. So, even if we click a button say 10 times, the callback will only be called once.

As for a throttle function, the callback function would be called immediately on the first call and it would be called more than once if we were to click the same button using a throttle function. However, for a throttle function it would not call the callback 10 times, rather it would add intervals of delay milliseconds between each call to the callback.

For example, if we were to click three times on a throttled button with a delay of 2 seconds, what would happen is that it would first call the callback function immediately, then it would set a timeout interval of 2 seconds. This means that the next call to the callback function cannot be invoked until 2 seconds have elapsed.

This is the core part of the throttling feature in a throttle function, that is, it waits the given delay milliseconds before executing the callback again.

Both functions are similar, but they do have a major difference, in that the debounce function will only be ever called once if we were to click multiple times, whereas the throtle function is called multiple times, each call having a delay that matches the delay specified when the function was called. There's a debounce v throttle visualizer on this website and it shows you the difference calls to the callback function using debounce or throttle.

Before moving onto the outline of a typical interview coding problem, let's look at some use cases for debounce and throttle. You would use a debounce function for a search bar, where you want to show suggestions based on a search term.

In this use case you only want to fire the search function when the user has stopped typing something, if you were to use a throttle function then this would not work properly as the search callback function would get called even when the user is typing.

For throttling, a typical use case is when you want a function to fire based on an event. For example, if you have an infinite scroll, whereby you append more content to the page as the user srolls down the page. In this scenario, a debounce function would not work, as it would only trigger when the user stops scrolling.

However, with a throttled function, we can fire an AJAX call every delay milliseconds as our user is scrolling down the page. Hopefully this clears up the differences between a debounce and a throttle function and typical use cases for each. Let's now have a look at the outline of typical throttle problem.

Outline of problem

  • Write a throttle function that takes in a required callback function and a required delay in milliseconds.
  • When throttle is called, it should return a new throttled version of the callback function. This callback function should take the same parameters as original callback
  • When the throttled version of the callback function is invoked for the first time, it should be called immediately without delay
  • Once the function has been called immediately, any further calls to the callback function should only be executed once the specified delay milliseconds has elapsed
  • If the callback function is invoked multiple times before the delay time has completed, it should only use the last call and pass that call's parameters to the callback function

Understanding the problem

  • Keep track of time
  • Track when function was last called
  • Check against delay if last called minus delay is less than delay then we call the callback otherwise we wait for remaining time
  • Spam clicks are ignored, only last click is used along with the parameters

Now to solve this problem, we'll need to do a few things. First we need to keep track of the last time a function was called, so we'll need a way of measuring the time elapsed. For this we'll need to make use of the built in timestamp function Date.now(), this function returns the current time in milliseconds since 1970. We'll use this to keep track of how much time has elapsed since the last call to the callback function.

Once we know the last call to the callback function, we can then check against the delay milliseconds, if there is still time left from the delay we're simply going to throttle the call to the callback function and wait until the delay time has elapsed. Another aspect we need to consider, is when the user continuously fires the callback function, in this case we need to ignore all of these calls except the last invokation, and we use that and pass any parameters along. Let's now look at the solution step by step.

Solution step by step

So now let's look at the solution for this problem:

function throttle(callback, delay) {
  // set the last call time to be 0, since the callback has not been called before
  let lastCalledTime = 0;
  let timeoutID;

  const throttledFunction = function (...args) {
    // get the current time now
    const currentTime = Date.now();
    // calculate the last time when the function was last called
    // to get this value, we need to subtract the lastCalledTime from the currentTime
    const timeSinceLastCall = currentTime - lastCalledTime;
    const delayRemaining = delay - timeSinceLastCall;
    if (delayRemaining <= 0) {
      lastCalledTime = currentTime;
      callback.apply(this, args);
    } else {
      clearTimeout(timeoutID);
      timeoutID = setTimeout(() => {
        lastCalledTime = Date.now();
        callback.apply(this, args);
      }, delayRemaining);
    }
  };

  return throttledFunction;
}

The function is similar to the debounce function we already looked at. We're using a closure again, and this time we're declaring a couple of variables:

function throttle(callback, delay) {
  // set the last call time to be 0, since the callback has not been called before
  let lastCalledTime = 0
  let timeoutID

  ...

the lastCalledTime variable will hold the last time the callback function was called, we need this so that we can determine we can call the callback function. We're setting it to be 0 here initially, and we'll see why in just a moment. The next variable is the timeoutID, this will hold the timeout ID returned by the setTimeout() function.

We're going to use this timeout ID like we did in the debounce example previously. Next we return a new function:

function throttle(callback, delay) {
  // set the last call time to be 0, since the callback has not been called before
  let lastCalledTime = 0
  let timeoutID

  const throttledFunction = function (...args) {
    // get the current time now
    const currentTime = Date.now()
    // calculate the last time when the function was last called
    // to get this value, we need to subtract the lastCalledTime from the currentTime
    const timeSinceLastCall = currentTime - lastCalledTime
    const delayRemaining = delay - timeSinceLastCall

We gather together all of the functon arguments using the rest operator into the args variable, as we'll need to pass these along to the callback function later when it's invoked. Then we start the tracking of the current time using the Date.now() function. This will return a number representing the milliseconds elapsed since 1 Jan 1970. So when the function is called this value is added to the currentTime variable.

We then need to work out the last time the callback functon was called. To get this value we subtract the lastCalledTime value from the currentTime value. At this point in the code execution we have a value for the lastCalledTime, this is 0 initially, so this means whatever currentTime is minus lastCalledTime.

The result is stored in a new variable timeSinceLastCall this variabe holds the time when the callback function was last called, we then use this variable to work out how much time, if any, we need to wait before calling the callback. To work out the remaining time, we subtract the value in lastCalledTime from the currentTime and store the value in delayRemaining this will give us the amount of time in milliseconds that we need to wait before calling the callback function again. This is the essence of the throttle function.

function throttle(callback, delay) {
  // set the last call time to be 0, since the callback has not been called before
  let lastCalledTime = 0
  let timeoutID

  const throttledFunction = function (...args) {
    // get the current time now
    const currentTime = Date.now()
    // calculate the last time when the function was last called
    // to get this value, we need to subtract the lastCalledTime from the currentTime
    const timeSinceLastCall = currentTime - lastCalledTime
    const delayRemaining = delay - timeSinceLastCall
    if (delayRemaining <= 0) {
      lastCalledTime = currentTime
      callback.apply(this, args)
    }

In the next part of the function, we create an if-block, this will check to see if we need to wait or immediately execute the callback function. We subtract the time since the last call - timeSinceLastCall - from the delay that was passed in, for example if we had passed in 3000 as the value for delay and we had ``4000as the value for thetimeSinceLastCall, then this will give a minus value of -1000` milliseconds. This is obviously less than 0, and it means that we no longer need to wait to call the callback again.

Once the value is less than or equal to 0, we enter the if-block, inside here we do a couple of things. We first re-assign the lastCalledTime, as we are now going to call the callback function - the time it is called has now change and this needs to be updated. As for the value that we use it is the currentTime since this the closest time to when the function is called. With the value for the lastCalledTime now updated we simply invoke the callback function, set the context for this and pass along any parameters using the args value.

Now let's look at the other situation, this is when we need to wait before calling the callback function:

function throttle(callback, delay) {
  // set the last call time to be 0, since the callback has not been called before
    ...

    } else {
      clearTimeout(timeoutID)
      timeoutID = setTimeout(() => {
        lastCalledTime = Date.now()
        callback.apply(this, args)
      }, delayRemaining)
    }
  }

  return throttledFunction
}

In this scenario, we handle the situation where the user spam clicks a button that's throttled for example, before the delay has elapsed, we want to ignore all the clicks except the last one. For this we use the clearTimeout function to clear any time out IDs that might be lingering. We then re-assign the timeoutID variable with a new timeout ID - just like we did for the debounce function.

However, as for the delay parameter for the setTimeout function, we set it to the value of delayRemaining and not delay. Since the last time the function was called might actually be less than delay, so we need to use this updated value. If delay was 3000 milliseconds and the time since last call is 2000 - then this means that we only need to wait 1000 and not the original 3000. We then simply return the new function and this meets all of the criteria in the problem outline above.

Conclusion

Both Throttle and debounce are very useful functions that will enhance the performance of JavaScript applications, and understanding why we need them and how they work will make you a better engineer. I hope this explanation of the throttle function was helpful and that you now have a better understanding of how it works.