DEV Community

Cover image for How to Use Throttle and Debounce in React to Increase Performance
Andrey Smirnov
Andrey Smirnov

Posted on • Edited on

How to Use Throttle and Debounce in React to Increase Performance

Overview

Throttle and Debounce solve optimization problems.

Throttle - skips function calls with a certain frequency.
Debounce - delays a function call until a certain amount of time has passed since the last call.

Throttle & Debounce scheme:

Examples of using Throttle:

1) If the user resizes the browser window and we need to change the content of the site.
Without optimization, the following happens. On each window resize event, the window resize event handler is called. So if the user, for example, resizes the window within 10 seconds, then 100, 200, and so on events that we need to process can happen.
Throttle allows us to set a time interval, more often than which the event handler will not be called. If we specify an interval of 1 second using Throttle, then the number of execution of window resize event handlers will be 10.

2) Showing the user the percentage of page scrolling. When the user scrolls the page, scroll events occur, which we need to handle. With throttle we can reduce the number of handled scroll events by setting a time interval.

Examples of using Throttle:

1) Processing the user's search query data.
When a user enters a search query, they are offered search options. It happens in the following way.
When changing the text entered by the user, a request will be sent to the server in which we transfer the characters already printed. Then we get a response from the server with possible search query options and show them to the user.
Each time the user changes the text, an event handler is called, in which a request is send to the server.
To optimize the number of requests sent to the server, we use Debounce.
When the text is changed by the user, using Debounce allows us to create a timer, for example for 1 second. If 1 second passes and the user has not changed the text a second time, then the event handler is called and the request will be sent to the server. If the user changes the text for the second time in 1 second, then the 1st timer is reset and a new timer is created again for 1 second.
Thus, if the user edits the search text quickly (less than 1 second), then the request will be sent to the server only once, after the user stops typing.
2) Sending analytics data to the server. For example, the user moves the mouse around the site, we write the mouse coordinates to an array, after which Debounce allows us to send information about the movement of the client's mouse to the server only after the client stops moving the mouse.

So, in this article, I'll show you how to use Throttle and Debounce in a React applications.

Step 1 - Application Template

Create an app template by using create-react-app and run it:

npx create-react-app throttle-debounce
cd throttle-debounce
npm start
Enter fullscreen mode Exit fullscreen mode

We replace the contents of the App.css file with our styles:

body {
  display: flex;
  justify-content: center;
  width: 100%;
}
h1 {
  text-align: center;
  margin: 0.5rem 0;
}
.l-scroll {
  overflow-y: scroll;
  overflow-x: hidden;
  width: 380px;
  height: 200px;
  margin-top: 0.5rem;
}
.scroll-content {
  width: 100%;
  background-color: bisque;
  padding: 0 1rem;
}
.l-scroll::-webkit-scrollbar {
  width: 10px;
  height: 8px;
  background-color: darkturquoise;
}
.l-scroll::-webkit-scrollbar-thumb {
  background-color: blueviolet;
}
Enter fullscreen mode Exit fullscreen mode

Let's replace the contents of the App.js file with our app template:

import './App.css';
import { useMemo } from 'react';

// High height scrollable content
const TallContent = () => {
  const dataElements = useMemo(() =>
    Array.from(
      {length: 200},
      (_, i) => <div key={i}>Line: {i+1}</div>
  ), []);

  return <>{dataElements}</>;
};

const App = () => {
  return (
    <>
      <h1>Throttle & Debounce</h1>
      <div className="l-scroll">
        <div className="scroll-content">
          <TallContent />
        </div>
      </div>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

The application template is ready, let's proceed to the second step - the usual scroll event handler.

Step 2 - Usual events hendler

Here we will add a usual events handler for scroll events and count the number of calls to this handler when the user scrolls the page element.

Let's add the state of the number of event handler invocations to the App component:

// At the beginning of the file
import { useState, useMemo } from 'react';
// Inside the App component
const [scrollHandleCount, setScrollHandleCount] = useState(0);
Enter fullscreen mode Exit fullscreen mode

Then we add a scroll event handler, for this we add the onScroll attribute to the element under the h1 heading:

// Before
<div className="l-scroll">
    ...
</div>

// After
<div className="l-scroll" onScroll={handleScroll}>
    ...
</div>
Enter fullscreen mode Exit fullscreen mode

We will also add a function for handling the handleScroll event to the App component:

const handleScroll = () => {
  handleUsualScroll();
};
Enter fullscreen mode Exit fullscreen mode

Inside the handleScroll function, we placed a function in which the usual event will be handled. Let's add this function to our App component:

const handleUsualScroll = () => {
  setScrollHandleCount(prevState => ++prevState);
};
Enter fullscreen mode Exit fullscreen mode

It remains only to show the status of the counter to the user, for this we add a line of code under the h1 heading:

<span>
  Usual scroll handle count: {scrollHandleCount}
</span>
<br />
Enter fullscreen mode Exit fullscreen mode

Now, when scrolling an element on the page, we should see the number of calls to the handleUsualScroll() function.

The full code of the App component at the moment:

const App = () => {
  const [scrollHandleCount, setScrollHandleCount] = useState(0);

  const handleUsualScroll = () => {
    setScrollHandleCount(prevState => ++prevState);
  };
  const handleScroll = () => {
    handleUsualScroll();
  };

  return (
    <>
      <h1>Throttle & Debounce</h1>
      <span>
        Usual scroll handle count: {scrollHandleCount}
      </span>
      <br />
      <div className="l-scroll" onScroll={handleScroll}>
        <div className="scroll-content">
          <TallContent />
        </div>
      </div>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Step 3 - Event Handler with Throttle

The Throttle event handler in our case should cause the scrollThrottleHandleCount counter to increment, while skip calls to increment the counter at certain intervals.
To implement our plans, we need a timer at the start of which the Throlle state goes into In progress. In this case, if the state is In Progerss, then the processing of user events (scrolling the page element) is skipped.
As soon as the timer fires, the Throttle state changes to Not in progress, which means our handler will process user events again. Thus, user events are skipped at a specified time interval.

We implement the above:

// Add useRef to store inProgress state
import { useState, useRef, useMemo, useEffect } from 'react';
Enter fullscreen mode Exit fullscreen mode

Next, in the App component, add the state of the event handler call counter with Throttle and ref to store the inProgress state:

// Number of event handler calls with Throttle
const [
  scrollThrottleHandleCount,
  setScrollThrottleHandleCount
] = useState(0);
// Keeping the state in progress
const throttleInProgress = useRef();
// Store the timer ID for its deletion
// when component unmount
const throttleTimerId = useRef();
Enter fullscreen mode Exit fullscreen mode

It is important to note here that throttleInProgress is part of the side effect associated with the timer, which means that we will store the state in the ref object, since useRef returns an object that exists throughout the entire life cycle of the component, while there is no extra rendering component when changing the current property of the object returned by useRef, as opposed to useState.
Now let's add the event handler itself with Throttle to the App component:

const handleThrottleScroll = () => {
  // If the state is inProgress - exit the function,
  // skip event processing
  if(throttleInProgress.current) return;
  // Set inProgress to true and start the timer
  throttleInProgress.current = true;
  throttleTimerId.current = setTimeout(() => {
    // Increment the throttleHandleCount
    // state by one
    setScrollThrottleHandleCount((prevState) => {
      return ++prevState;
    });
    // Set inProgress to false, which means
    // that setTimeout will work
    // again on the next run
    throttleInProgress.current = false;
  }, 500);
};
Enter fullscreen mode Exit fullscreen mode

There are 2 simple steps left: add a display of the state of the counter with Throttle to the user and add handleThrottleScroll() to handleScroll():

// After heading h1
<span>
  Throttle scroll handle count: {scrollThrottleHandleCount}
</span>

// In the handleScroll() function after handleUsualScroll();
handleThrottleScroll();
Enter fullscreen mode Exit fullscreen mode

As a result, we will get:

The normal event handler called the business logic of the application 181 times, and with Throttle only 9.
The complete code for the App component with Throttle:

const App = () => {
  const [scrollHandleCount, setScrollHandleCount] = useState(0);
  const [
    scrollThrottleHandleCount,
    setScrollThrottleHandleCount
  ] = useState(0);
  const throttleInProgress = useRef(false);
  const throttleTimerId = useRef();

  useEffect(() => {
    // Clear the timer when unmounting a component
    return () => {
      if (throttleTimerId.current) {
        clearTimeout(throttleTimerId.current);
      }
    };
  }, []);

  const handleUsualScroll = () => {
    setScrollHandleCount(prevState => ++prevState);
  };
  const handleThrottleScroll = () => {
    if(throttleInProgress.current) return;
    throttleInProgress.current = true;
    throttleTimerId.current = setTimeout(() => {
      setScrollThrottleHandleCount(prevState => ++prevState);
      throttleInProgress.current = false;
    }, 500);
  };
  const handleScroll = () => {
    handleUsualScroll();
    handleThrottleScroll();
  };

  return (
    <>
      <h1>Throttle & Debounce</h1>
      <span>
        Usual scroll handle count: {scrollHandleCount}
      </span>
      <br />
      <span>
        Throttle scroll handle count: {scrollThrottleHandleCount}
      </span>
      <br />
      <div className="l-scroll" onScroll={handleScroll}>
        <div className="scroll-content">
          <TallContent />
        </div>
      </div>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Let's move on to the final step - implement the Debounce event handler.

Step 4 — Event Handler with Debounce

Debounce in our example delays incrementing the scrollDebounceHandleCount counter until a certain amount of time has elapsed since the last call of the event handler***.
Let's add the state of the number of calls to the event handler with Debounce, ref to store the timer ID in the App component:

const [
  scrollDebounceHandleCount,
  setScrollDebounceHandleCount
] = useState(0);
const debounceTimerIdRef = useRef();
Enter fullscreen mode Exit fullscreen mode

Then we show the number of scrollDebounceHandleCount to the user and add our handleDebounceScroll() method to handleScroll():

// After h1
<span>
  Debound scroll handle count: {scrollDebounceHandleCount}
</span>
// In handleScroll() function
handleDebounceScroll();
Enter fullscreen mode Exit fullscreen mode

It remains to write the handleDebounceScroll function:

const handleDebounceScroll = () => {
  // If the timer ID is set, reset the timer
  if(debounceTimerIdRef.current){
    clearTimeout(debounceTimerIdRef.current);
  }
  // We start the timer, the returned timer ID
  // is written to debounceTimerIdRef
  debounceTimerIdRef.current = setTimeout(() => {
    // Increasing the counter for the number of
    // executions of the business logic
    // of the application with Debounce
    setScrollDebounceHandleCount((prevState) => {
      return ++prevState;
    });
  }, 500);
};
Enter fullscreen mode Exit fullscreen mode

As a result, the Debounce counter increments only when the user stops scrolling the page element for more than or equal to 500 milliseconds:

The full text of the App component:

const App = () => {
  const [scrollHandleCount, setScrollHandleCount] = useState(0);
  const [
    scrollThrottleHandleCount,
    setScrollThrottleHandleCount
  ] = useState(0);
  const [
    scrollDebounceHandleCount,
    setScrollDebounceHandleCount
  ] = useState(0);

  const throttleInProgress = useRef();
  const throttleTimerId = useRef();
  const debounceTimerIdRef = useRef();

  useEffect(() => {
    return () => {
      if (throttleTimerId.current) {
        clearTimeout(throttleTimerId.current);
      }
      if (debounceTimerIdRef.current) {
        clearTimeout(debounceTimerIdRef.current)
      }
    };
  }, []);

  const handleUsualScroll = () => {
    setScrollHandleCount(prevState => ++prevState);
  };
  const handleThrottleScroll = () => {
    if(throttleInProgress.current){ return; }
    throttleInProgress.current = true;
    throttleTimerId.current = setTimeout(() => {
      setScrollThrottleHandleCount(prevState => ++prevState);
      throttleInProgress.current = false;
    }, 500);
  };
  const handleDebounceScroll = () => {
    if(debounceTimerIdRef.current){
      clearTimeout(debounceTimerIdRef.current);
    }
    debounceTimerIdRef.current = setTimeout(() => {
      setScrollDebounceHandleCount(prevState => ++prevState);
    }, 500);
  };
  const handleScroll = () => {
    handleUsualScroll();
    handleThrottleScroll();
    handleDebounceScroll();
  };

  return (
    <>
      <h1>Throttle & Debounce</h1>
      <span>
        Usual scroll handle count: {scrollHandleCount}
      </span>
      <br />
      <span>
        Throttle scroll handle count: {scrollThrottleHandleCount}
      </span>
      <br />
      <span>
        Debound scroll handle count: {scrollDebounceHandleCount}
      </span>
      <div className="l-scroll" onScroll={handleScroll}>
        <div className="scroll-content">
          <TallContent />
        </div>
      </div>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Subscribe to the blog, put likes, add to bookmarks.
Don't forget about unicorns.

Thanks for your attention!

Top comments (2)

Collapse
 
nssimeonov profile image
Templar++

Awesome explanation!

While we all have timeouts in the background one way or another, I think it's better to avoid reinventing the wheel again and again and use this excellent lib: github.com/xnimorz/use-debounce

Collapse
 
andreyen profile image
Andrey Smirnov

Yes, in production, of course. This article is designed to explain the working mechanism for beginners.