3

Throttle in JavaScript: Improve Your Application’s Performance | Skilled.dev

 2 years ago
source link: https://skilled.dev/course/throttle
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Interview Question

Throttle

Log in to continue or upgrade to the full course

A throttle is a common technique used in the browser to improve an application's performance by limiting the number of events your code needs to handle. You should use a throttle when you want to execute a callback at a controlled rate, allowing you to handle intermediate states repeatedly at each fixed interval of time.

throttle-scrolling.gif

On the left, side we have normal events that aren't throttled and the event handler is executed each moment the page is scrolled. On the right, we have a throttled event handler and will only execute the function once every 1 second.

Implementing a throttle from scratch is a common interview question. It tests your understanding of intermediate and advanced JavaScript concepts such as async programming, callbacks, scope, and closures. It is also a practical solution used in real-world applications to improve performance and demonstrates that you understand the tools to write good code for real users.

A throttle is a cousin of the debounce, and they both improve the performance of web applications. However, they are used in different cases. A debounce is utilized when you only care about the final state. For example, waiting until a user stops typing to fetch typeahead search results. A throttle is best used when you want to handle all intermediate states but at a controlled rate. For example, track the screen width as a user resizes the window and rearrange page content while it changes instead of waiting until the user has finished.

Write a function throttle that takes a callback function and a delay time and limits the callback from executing to once for each window of delay milliseconds.

If the function is executed multiple times, throttle should handle it every delay milliseconds in evenly spaced increments. You only care about the most recent event and should ignore any previous ones if a new value comes in.

const WAIT_TIME = 5000; // 5 seconds
const throttledEventListiner = throttle(() => console.log('I executed'), WAIT_TIME);

// time = 0
throttledEventListiner();
// wait 2 seconds (time = 2)
throttledEventListiner();
// wait 2 seconds (time = 4)
throttledEventListiner();
// wait 2 seconds (time = 6)
throttledEventListiner();
// wait 2 seconds (time = 8)
throttledEventListiner();

// Executes every 5 seconds even though it's been called more times
executes callback at     |     |     |
time                     0     5     10

Throttling in the Context of Web Development

Imagine you have a scroll event handler where you want to display new content to a user as they move down the page. If we executed the callback every time the user scrolls a single pixel, we would quickly get clogged with events if they rapidly scroll because it would send hundreds or thousands of events in quick succession. Instead, we throttle it so that we only check the distance scrolled every 100ms so that we're getting only 10 callbacks per second. The response will still feel instantaneous to the user but be much more efficient computationally.

A throttle creates evenly-spaced function calls. Imagine if you were performing some heavy calculation or an API request in the event handler callback function. By throttling these callbacks, you prevent the app from freezing or unnecessarily hammering your server with requests.

Throttle Implementation in JavaScript

Let's immediately jump into the code for a throttle. I'll describe it below and then also provide a commented version of the function.

const throttle = (callback, delay) => {
  let throttleTimeout = null;
  let storedEvent = null;

  const throttledEventHandler = (event) => {
    storedEvent = event;

    const shouldHandleEvent = !throttleTimeout;

    if (shouldHandleEvent) {
      callback(storedEvent);

      storedEvent = null;

      throttleTimeout = setTimeout(() => {
        throttleTimeout = null;

        if (storedEvent) {
          throttledEventHandler(storedEvent);
        }
      }, delay);
    }
  };

  return throttledEventHandler;
};

A throttle is a higher-order function, which is a function that returns another function (named throttledEventHandler here for clarity). This is done to form a closure around the callback, delay, throttleTimeout, and storedEvent function parameters. This preserves the values of each variable to be read when throttledEventHandler is executed. The following is a definition of each variable:

  • callback: The throttled function that you want to execute at a given rate.
  • delay: The amount of time you want the throttle function to wait between executions of the callback.
  • throttleTimeout: The value used to indicate a running throttle created by our setTimeout.
  • storedEvent: The event that you want to handle with the throttled callback. This value will continually be updated until the throttle ends. Then it will execute callback with the most recent value.

We can use our throttle as in the following code:

var returnedFunction = throttle(function() {
  // Do all the taxing stuff and API requests
}, 500);

window.addEventListener('scroll', returnedFunction);

Since a throttle returns a function, the throttledEventHandler from the first example and the returnedFunction function from the second example are actually the same function. Every time a user scrolls, it will execute throttledEventHandler/returnedFunction.

Let's step through what happens when we throttle a function. First we create a closure around variables so they will be available to throttledEventHandler on every execution. throttledEventHandler receives 1 argument which is the event. It stores this event in the storedEvent variable.

Then we check if we have a timeout running (ie. an active throttle). If we do have a throttle, then throttledEventHandler has completed this execution and waits to execute the callback. If the throttle is not active, we are able to immediately handle the event with our callback function. We then call setTimeout and store the timeout value which indicates our throttle is running.

While the timeout is active, the most recent event is always stored. The callback execution is bypassed which saves us from performing CPU intensive tasks or calling our API.

When the setTimeout finishes, we null out the throttleTimeout indicating that the function is no longer throttled and can handle events. If there is a storedEvent, we want to handle it immediately, and to do this we call throttledEventHandler recursively. The recursive call inside the setTimeout is what allows us to handle events at a constant rate. It will repeatedly execute the same process after a desired delay as long as new events continue to come in.

See a throttle in action and play around with it in this Codepen.

A commented version of the function as well:

// Pass in the callback that we want to throttle and the delay between throttled events
const throttle = (callback, delay) => {
  // Create a closure around these variables.
  // They will be shared among all events handled by the throttle.
  let throttleTimeout = null;
  let storedEvent = null;

  // This is the function that will handle events and throttle callbacks when the throttle is active.
  const throttledEventHandler = (event) => {
    // Update the stored event every iteration
    storedEvent = event;

    // We execute the callback with our event if our throttle is not active
    const shouldHandleEvent = !throttleTimeout;

    // If there isn't a throttle active, we execute the callback and create a new throttle.
    if (shouldHandleEvent) {
      // Handle our event
      callback(storedEvent);

      // Since we have used our stored event, we null it out.
      storedEvent = null;

      // Create a new throttle by setting a timeout to prevent handling events during the delay.
      // Once the timeout finishes, we execute our throttle if we have a stored event.
      throttleTimeout = setTimeout(() => {
        // We immediately null out the throttleTimeout since the throttle time has expired.
        throttleTimeout = null;

        // If we have a stored event, recursively call this function.
        // The recursion is what allows us to run continusously while events are present.
        // If events stop coming in, our throttle will end.
        // It will then execute immediately if a new event ever comes.
        if (storedEvent) {
          // Since our timeout finishes:
          // 1. This recursive call will execute `callback` immediately since throttleTimeout is now null
          // 2. It will restart the throttle timer, allowing us to repeat the throttle process
          throttledEventHandler(storedEvent);
        }
      }, delay);
    }
  };

  // Return our throttled event handler as a closure
  return throttledEventHandler;
};

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK