Make your React Apps more performant using Debouncing & Throttling! ๐Ÿ”ฅ๐Ÿš€

Make your React Apps more performant using Debouncing & Throttling! ๐Ÿ”ฅ๐Ÿš€

A simple guide to optimizing your React Apps performance using Debouncing & Throttling.

ยท

11 min read

Well, hello there! ๐Ÿ‘‹ I see you have come here to learn more about how to make your React apps performant and optimize them using Debouncing and Throttling, which is great because that means you really do care about your app's performance. Kudos for that! ๐Ÿ‘

Do note that this blog assumes that you have a basic understanding of how React works and that you are familiar with React Hooks.

Before we jump in, let's understand why would you want to optimize your React app's performance?

Suppose you have a very simple React app with an input bar to search cities like the below,

Debouncing-Throttling-demo-1.gif

As you can see, this app is super laggy and the UX of this app is ๐Ÿ’ฉ. We are just making a very simple search that filters cities from a list of cities based on user input.

PS:- You can give it a try if you want (Please do it at your own risk, you don't wanna hang your computer!) - codesandbox.io/s/debouncing-example-demo-0d..

Now, you might ask why's this React app so laggy?

Why's the app super laggy?

If you would have noticed carefully from the above demo of the app, we are filtering the cities from the list of cities that we have on every keystroke made by the user (notice the keystrokes on the Virtual keyboard in the demo).

See now, that's not a performant app at all and needs to be optimized to provide a better user experience.

Let's have a look at two ways to optimize such apps and make them better!

Let's make our React apps better

What is Debouncing & Throttling?

There are many scenarios that make your app less performant like making API calls on each user keystroke on an input search bar, performing compute-heavy operations on button clicks, window resizing, or frequent scrolls on a scrollbar.

Basically, any scenario in which you are making expensive (in terms of computing or execution time) function calls on events or user actions that can hamper the performance of your apps.

Now, let's understand Debouncing & Throttling.

Debouncing: In debouncing, we try to reduce the number of expensive function calls by calling them only if the time difference between two consecutive event triggers (user actions) is greater than or equal to a specified delay. This delay can be adjusted depending on the use case or the kind of user experience you are trying to design for your app.

Throttling: In throttling, we try to rate-limit the number of expensive function calls by calling them every time only after a certain time limit has passed from the last function call. Again, this time limit can be adjusted depending on your use case.

Debouncing & Throttling can be very useful to handle rate-limit errors caused by rate-limiting on certain APIs that your apps might be consuming, as we are trying to reduce the number of such expensive function calls using these optimizations.

Now that you have some idea about Debouncing & Throttling, let's dive deeper into each concept using a simple example that illustrates some of their common use cases.

Optimizing apps using Debouncing

Let's go back to the very first example that we saw, where we had a simple search bar that filters the cities from a list of cities based on the user input.

We can use debouncing in this case to reduce the number of function calls to filter the cities from the list.

But first, let's look at the initial code from the demo.

Initial Code -

import "./styles.css";
import React, { useState } from "react";
import cities from "cities-list";
import { v4 as uuidv4 } from "uuid";

// An array of city names
const citiesArray = Object.keys(cities);

export default function App() {
  const [cityInput, setCityInput] = useState("");
  const [filteredCities, setFilteredCities] = useState([]);

  // Function that filters cities from the list based on user input
  const cityFilter = (query) => {
    console.log(query);
    if (!query) return setFilteredCities([]);

    setFilteredCities(
      citiesArray.filter((city) =>
        city.toLowerCase().includes(query.toLowerCase())
      )
    );
  };

  return (
    <div className="App">
      <h1 className="app-header">Find cities</h1>
      <div className="city-input">
        <input
          type="text"
          value={cityInput}
          onChange={(e) => {
            setCityInput(e.target.value);
            cityFilter(e.target.value);
          }}
        />
      </div>
      <div>
        {filteredCities.map((city) => {
          return <div key={uuidv4()}>{city}</div>;
        })}
      </div>
    </div>
  );
}

The above code snippet represents a simple React component with an input search bar, and a container that displays the filtered cities.

// Function that filters cities from the list based on user input
  const cityFilter = (query) => {
    console.log(query);
    if (!query) return setFilteredCities([]);

    setFilteredCities(
      citiesArray.filter((city) =>
        city.toLowerCase().includes(query.toLowerCase())
      )
    );
  };

The function cityFilter takes a user search query as an input parameter and filters the cities from a list of cities (fetched from an npm package called cities-list). Currently, this function runs on every single keystroke made by the user on the search bar.

Now, let's write a debounced version of the above cityFilter function to make it more optimal. We will be using setTimeout in JavaScript to achieve this.

// `timer` to help while clearing setTimeout 
// inside `debouncedCityFilter` function
let timer;

// Debounced version of the `cityFilter` func to filter cities 
// based on user search query
  const debouncedCityFilter = (query) => {
    clearTimeout(timer);
    if (!query) return setFilteredCities([]);

    timer = setTimeout(() => {
      console.log(query);

      setFilteredCities(
        citiesArray.filter((city) =>
          city.toLowerCase().includes(query.toLowerCase())
        )
      );
    }, 500);
  };

According to the concept of debouncing, we make function calls only if the time difference between two consecutive event triggers (user actions) is greater than or equal to a specified delay.

In the above code snippet, we set the state to get the filtered cities using setFilteredCities() which is called inside a setTimeout with a delay of 500ms(this delay can be adjusted according to the use case). So whenever a user keystroke is recorded on the input search bar, the debouncedCityFilter function is called which triggers setTimeout and sets the state using setFilteredCities() after 500ms.

However, if another keystroke made by the user is recorded just within this time delay of 500ms, the previous setTimeout needs to be cleared to avoid filtering the cities and set the state. For this, we use clearTimeout that takes the id returned by the setTimeout function.

Now, this id needs to be preserved so that it is available whenever we need to use clearTimeout to clear the timer. We use a quite popular concept called Closures in JavaScript to be able to access this id inside the debouncedCityFilter function. Hence, if you'd have noticed we have defined a timer variable outside the debouncedCityFilter function for use inside this function.

By simply debouncing the cityFilter function, we are able to reduce the number of function calls and hence able to improve the performance significantly of our React app.

Let's have a look at what our React component code looks like after making these changes.

Final Code -

import "./styles.css";
import React, { useState } from "react";
import cities from "cities-list";
import { v4 as uuidv4 } from "uuid";

// An array of city names
const citiesArray = Object.keys(cities);

// `timer` to help while clearing setTimeout 
// inside `debouncedCityFilter` function
let timer;

export default function App() {
  const [cityInput, setCityInput] = useState("");
  const [filteredCities, setFilteredCities] = useState([]);

  // Function that filters cities from the list based on user input
  const cityFilter = (query) => {
    console.log(query);
    if (!query) return setFilteredCities([]);

    setFilteredCities(
      citiesArray.filter((city) =>
        city.toLowerCase().includes(query.toLowerCase())
      )
    );
  };

  // Debounced version of the `cityFilter` func to filter 
  // cities based on user search query
  const debouncedCityFilter = (query) => {
    clearTimeout(timer);
    if (!query) return setFilteredCities([]);

    timer = setTimeout(() => {
      console.log(query);

      setFilteredCities(
        citiesArray.filter((city) =>
          city.toLowerCase().includes(query.toLowerCase())
        )
      );
    }, 500);
  };

  return (
    <div className="App">
      <h1 className="app-header">Find cities</h1>
      <div className="city-input">
        <input
          type="text"
          value={cityInput}
          onChange={(e) => {
            setCityInput(e.target.value);
            debouncedCityFilter(e.target.value);
          }}
        />
      </div>
      <div>
        {filteredCities.map((city) => {
          return <div key={uuidv4()}>{city}</div>;
        })}
      </div>
    </div>
  );
}

Now, have a look at how debouncing has improved the performance of this component significantly! ๐Ÿš€

If you want to handle more edge cases for debouncing such functions, then you can check out Lodash which has a debounce method that covers most of the edge cases involved to make such functions more optimal.

Now, let's look at a simple example that uses Throttling to make it more performant.

Optimizing apps using Throttling

Let's suppose, you have a simple React component consisting of a button that on clicking calls an API to fetch some data related to all the currencies of different countries.

Initial Code -

import "./styles.css";
import React, { useState } from "react";
import axios from "axios";
import { v4 as uuid } from "uuid";

export default function App() {
  const [currencyData, setCurrencyData] = useState({});
  const [clickCounter, setClickCounter] = useState(0);

  const getCurrencyData = async () => {
    console.log("Fetching data ....");

    const { data } = await axios.get(
      "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies.json"
    );

    // Fetching only 15 currencies for now
    const countryCurrencies = {};
    const currencyObjKeys = Object.keys(data).slice(0, 15);

    currencyObjKeys.forEach((key) => {
      countryCurrencies[key] = data[key];
    });

    setCurrencyData({ ...countryCurrencies });
  };

  return (
    <div className="App">
      <h1>Currencies of different Countries</h1>
      <button
        className="currency-btn"
        onClick={() => {
          setClickCounter((clickCount) => clickCount + 1);
          getCurrencyData();
        }}
      >
        Click to get all currencies
      </button>
      <span>Btn clicked - {clickCounter} times</span>
      <div className="currencies">
        {Object.keys(currencyData).map((currency) => {
          return (
            <div key={uuid()}>
              {currency}: {currencyData[currency]}
            </div>
          );
        })}
      </div>
    </div>
  );
}

The code snippet above is our simple component with two states - currencyData & clickCounter. On button click, we update the clickCounter state to reflect the total number of button clicks made so far and also call the getCurrencyData() function to make an API call to fetch the currency data.

Let's look at what this component looks like!

Debouncing-&-Throttling-demo-2.gif

As you might have noticed above, each button click triggers an API call. Now, imagine that your app was used by hundreds or thousands of users, the number of API calls would be humongous! Your Back-End servers could face a huge pool of requests coming from each user due to so many clicks. Also, if you are consuming any external paid API or service, then the endpoints might start throwing errors because of rate-limiting on the API endpoints.

Even if say you were not making any API calls on such button clicks but rather performing some compute-heavy operation, it would hamper your app's performance severely!

Now, that's a bit of a problem ๐Ÿ˜…

Let's try and solve this problem using Throttling! โœจ

We will throttle the getCurrencyData function that makes an API call on each button click.

Currently, the code for getCurrencyData looks like this,

const getCurrencyData = async () => {
    console.log("Fetching data ....");

    const { data } = await axios.get(
      "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies.json"
    );

    // Fetching only 15 currencies for now
    const countryCurrencies = {};
    const currencyObjKeys = Object.keys(data).slice(0, 15);

    currencyObjKeys.forEach((key) => {
      countryCurrencies[key] = data[key];
    });

    setCurrencyData({ ...countryCurrencies });
  };

Now, we will write a function throttledGetCurrencyData that will throttle and use the getCurrencyData function to reduce the number of calls made to it.

// A flag to control the function calls to the `getCurrencyData` function
let shouldFuncBeCalled = true;

const throttledGetCurrencyData = async () => {
    if (shouldFuncBeCalled) {
      await getCurrencyData();
      shouldFuncBeCalled = false;

      setTimeout(() => {
        shouldFuncBeCalled = true;
      }, 500);
    }
  };

The throttledGetCurrencyData function calls the getCurrencyData function only if the shouldFuncBeCalled flag is set to true. Once this function is called, we delay the next function call to the getCurrencyData function by using setTimeout with some specific delay (This delay limit can be adjusted as per your use case).

This way we only allow function calls to happen after a certain amount of time has passed from the last function call. This way we can avoid making the UI slow or crossing the rate limits defined for any API that your app might be consuming.

Let's look at how the app is working now.

Debouncing-&-Throttling-demo-3.gif

As you can see from the console, the number of API calls has been reduced quite significantly even after clicking the button so many times!

Check out the CodeSandbox below to see what the code for our component looks like after using Throttling.

If you want to handle more edge cases for throttling such functions, then you can check out Lodash which has a throttle method that covers most of the edge cases involved to make such functions more optimal.

Debouncing vs Throttling, when to use what?

Now that we understand how Debouncing and Throttling work, let's understand some of the differences and when to use Debouncing or Throttling.

Throttling enforces that a function must be called every time after a certain amount of time (or delay) has passed from the last function call.

Whereas, Debouncing enforces that a function must only be called if a certain amount of time (or delay) has passed without it being called. If this time has not been passed the debounce timer keeps resetting and the function call is avoided.

When to use what?

  • Search bar: Use debouncing to avoid searching every time a user hits a keystroke. Throttling is not convenient here to use in this scenario, as you don't want to make your user wait too long for fetching the search results (in the worst case if the previous function call was made just when the user stops typing).

  • Shooting game: Use throttling on mouse click as shooting a pistol takes a few secs to register and it helps in avoiding the user to shoot until the previous shot has been registered. Debouncing will not shoot a bullet until a certain amount of time has passed when the pistol was not fired.

You can also check out this amazing Stackoverflow post to understand the differences between Debouncing & Throttling and when to use what.

Conclusion

Debouncing & Throttling are just a few ways you can make your React apps more performant & each technique has its own set of pros and cons depending on the use cases. In this blog, we first talked about Why should we care about our React app's performance, then we understood how we can use Debouncing & Throttling to optimize our app's performance, and finally saw a major difference between the two techniques and when we to use which technique.

That's it from me folks, thank you so much for reading this blog! ๐Ÿ™Œ I hope this blog was helpful and gave you an insight on how you can make your React apps more performant. Now, go ahead and make your apps even more amazing! ๐Ÿš€

Until next time!

Feel free to reach out to me:
Twitter
Linkedin
GitHub
You can also reach out to me over mail: devansuyadav@gmail.com

ย