5

Throttle and Debounce in Javascript and React

 1 year ago
source link: https://codefrontend.com/debounce-throttle-js-react/
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.
Throttle and Debounce in Javascript and React

Both throttle and debounce are used to optimize expensive, frequently executed actions. They're two of the most common performance optimization techniques.

I'll show you how to implement them yourself and how to use them in JavaScript and React.

What is debouncing?

Debouncing is a technique used to improve the performance of frequently executed actions, by delaying them, grouping them, and only executing the last call.

Picture this: you want a search input where results are queried automatically as you're typing into it.

You wouldn't want to ping the backend on every keystroke, rather you only care about the final value. That's the perfect use case for a debounce function, in fact it's a very common one.

Use the debounce function when you only care about the final result of the expensive action.

Implementing a basic debounce function

Debounce is implemented using a timer, where the action executes after the timeout. If the action is repeated before the timer has finished, we restart the timer and queue the latest action.

Here's the most basic implementation in TypeScript:

function debounce<Args extends unknown[]>(fn: (...args: Args) => void, delay: number) {
  let timeoutID: number | undefined;

  const debounced = (...args: Args) => {
    clearTimeout(timeoutID);
    timeoutID = window.setTimeout(() => fn(...args), delay);
  };

  return debounced;
}

Simple debounce function. 

  1. Create a function that accepts a function to debounce and the timeout delay as arguments.
  2. The debounce function returns a new function. When it's executed, it creates a timer to execute the original function after the delay and cancels the previous timer.

Here's how to use it:

const expensiveCalculation = debounce(() => {
  // 🚩 Do the expensive calculation
}, 1000)

// 👇 Frequently called function
function onChange() {
  // 👇 Will run at most once per second
  expensiveCalculation()
}

Example debounce usage.

Flushing the debounce result

What if we sometimes need to call the action before the delay and cancel any pending executions? We call that flushing.

We can attach an extra method to  the original debounce function implementation, that runs the pending action instantly and clears the timer:

function debounce<Args extends unknown[]>(fn: (...args: Args) => void, delay: number) {
  let timeoutID: number | undefined;
  let lastArgs: Args | undefined;

  const run = () => {
    if (lastArgs) {
      fn(...lastArgs);
      lastArgs = undefined;
    }
  };

  const debounced = (...args: Args) => {
    clearTimeout(timeoutID);
    lastArgs = args;
    timeoutID = window.setTimeout(run, delay);
  };

  debounced.flush = () => {
    clearTimeout(timeoutID);
    run();
  };

  return debounced;
}

Debounce function with flush support.

  1. Store the arguments of the last action into an array when calling the debounced function.
  2. Create a new debounce.flush function that runs the action with the most recently used arguments and clears the timer and cached arguments.

Call debounce.flush() to run the action immediately:

const expensiveCalculation = debounce(() => {
  // Expensive calculation...
}, 1000)

function onChange() {
  expensiveCalculation()
}

function onClose() {
  // 👇 Instantly runs the calculation and cancels any pending calls
  expensiveCalculation.flush()
}

Flushing the debounce function.

What is throttling?

Throttling is a technique used to improve the performance of frequently executed actions, by limiting the rate of execution. It is similar to debounce, except it guarantees the regular execution of an action.

The most common use case from my experience is to optimize the resize and scroll handlers. That's especially important in React because they often trigger state updates that are responsible for triggering re-renders of your components.

The solution to this problem is to call the handlers intermittently. Most of the time we don't need to keep 100% in sync with the resize or scroll events, so we can call throttle the handler functions. I like to think of them as lossy event handlers.

Use the throttle function when you care about some intermediate values of a frequently executed expensive action, but it's ok to discard most of them.

Implementing a throttle function

The throttle function is implemented using a timer that puts the throttled function on cooldown:

  1. Create a throttle function that accepts a callback and the cooldown duration arguments.
  2. The throttle function returns a new function, which when executed, stores the call arguments and starts the cooldown timer.
  3. When the timer finishes, execute the action with the cached arguments and clear them.
function throttle<Args extends unknown[]>(fn: (...args: Args) => void, cooldown: number) {
  let lastArgs: Args | undefined;

  const run = () => {
    if (lastArgs) {
      fn(...lastArgs);
      lastArgs = undefined;
    }
  };

  const throttled = (...args: Args) => {
    const isOnCooldown = !!lastArgs;

    lastArgs = args;

    if (isOnCooldown) {
      return;
    }

    window.setTimeout(run, cooldown);
  };

  return throttled;
}

The throttle function code snippet.

Here's how you would use it:

const expensiveCalculation = throttle(() => {
  // 🚩 Do the expensive calculation
}, 100)

function onResize() {
  // 👇 Will be called once every 100ms
  expensiveCalculation()
}

Example throttle usage.

If you use lodash, you already have access to both debounce and throttle functions.

Using throttle and debounce in React

In React, new functions are created every time the component re-renders, which is not great for our debounce/throttle implementation which relies on the closure staying the same.

When you use debounce and throttle in React, make sure to wrap them with useMemo hook:

const handleChangeText = useMemo(() =>
  debounce((e: ChangeEvent<HTMLInputElement>) => {
    // Handle the onChange event
  }, 1000),
[]);

const handleWindowResize = useMemo(() =>
  throttle(() => {
    // Handle the onResize event
  }, 100),
[]);

Wrapping the debounce and throttle with useMemo.

Custom useDebounce and useThrottle hooks

You can also turn this into custom react hooks:

import { DependencyList, useMemo } from 'react';
import debounce from './debounce';
import throttle from './throttle';

function useDebounce<Args extends unknown[]>(
  cb: (...args: Args) => void,
  delay: number,
  deps: DependencyList,
) {
  return useMemo(() => debounce(cb, delay), deps);
}

function useThrottle<Args extends unknown[]>(
  cb: (...args: Args) => void,
  cooldown: number,
  deps: DependencyList,
) {
  return useMemo(() => throttle(cb, cooldown), deps);
}

Code snippet for useDebounce and useThrottle hooks.

Debounce example in React

Here's how you could use the custom useDebounce hook in React:

import { ChangeEvent, useState } from 'react';
import useDebounce from './useDebounce';

function DebounceWithFlushExample() {
  const [text, setText] = useState('');

  const handleChangeText = useDebounce(
    (e: ChangeEvent<HTMLInputElement>) => {
      setText(e.target.value);
    },
    5000,
    [],
  );

  return (
    <div>
      <div>Text (5 second): {text}</div>
      <input type="text" onChange={handleChangeText} />
      <button onClick={handleChangeText.flush}>Flush</button>
    </div>
  );
}

Example of using debounce in React.

Throttle example in React

Here's how you could use the custom useThrottle hook in React:

import { useEffect, useState } from 'react';
import useThrottle from './useThrottle';

type Range = 'small' | 'medium' | 'large';

const sizeToRange = (size: number): Range => {
  if (size < 600) {
    return 'small';
  } else if (size > 1200) {
    return 'large';
  }
  return 'medium';
};

function ThrottleExample() {
  const [range, setRange] = useState(sizeToRange(window.innerWidth));

  const handleWindowResize = useThrottle(
    () => {
      // Execute some expensive operation
      setRange(sizeToRange(window.innerWidth));
    },
    100,
    [],
  );

  useEffect(() => {
    window.addEventListener('resize', handleWindowResize);
    return () => {
      window.removeEventListener('resize', handleWindowResize);
    };
  }, [handleWindowResize]);

  return <div>Screen size (resize to see): {range}</div>;
}

export default ThrottleExample;

Example of using throttle in React.

Here are both examples in CodeSandbox so you can play around with them:

When to use throttling and when to debounce?

Use debounce when you don't care about the intermediate results because the action only makes sense with the last result. An example of that is - search results.

Use throttle when you need intermediate results, but a small delay is acceptable. For example, resizing or scrolling.


As always, you can find the code examples in my GitHub repository. Next, learn to upload files with react:

file-upload-article-cover.png




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK