DEV Community

Cover image for Debounce, Performance and React
Jason
Jason

Posted on

Debounce, Performance and React

Debounce, Performance and React

While "debounce" is a more broad software development pattern, this article will focus on debounce implemented in React.

What is Debounce?

Debounce is a way of delaying some peice of code, until a specified time to avoid unnecessary CPU cycles and increase software performance.

Why does it matter?

Performance.

Debounce allows us to increase application performance by limit the frequency of "expensive operations".

Specifically, operations that require significant resources (CPU, Memory, Disk) to execute. "Expensive operations" or slow application load times, cause freezes and delays in the user-interface, and require more of your network than is ultimately necessary.

Understanding through example

Debounce makes the most sense in context.

Imagine we have a simple movie search application:

import React, { useState } from "react";
import Axios from "axios"; // to simplify HTTP request 
import "./App.css";

/**
 * Root Application Component
 */
export default function App() {
  // store/update search text & api request results in state
  const [search, setSearch] = useState("");
  const [results, setResults] = useState([]);

  /**
   * Event handler for clicking search
   * @param {event} e
   */
  const handleSearch = async (e) => {
    e.preventDefault(); // no refresh
    try {
      const searchResults = await searchAny(search);
      await setResults(searchResults.Search);
    } catch (error) {
      alert(error);
    }
  };

  return (
    <div className="app">
      <header>React Movies</header>
      <main>
        <Search value={search} setValue={setSearch} onSearch={handleSearch} />
        <Movies searchResults={results} />
      </main>
    </div>
  );
}

/**
 * Movie Card component
 * @param {{movie}} props with movie object containing movie data
 */
function MovieCard({ movie }) {
  return (
    <div className="movieCard">
      <h4>{movie.Title}</h4>
      <img alt={movie.Title} src={movie.Poster || "#"} />
    </div>
  );
}

/**
 * Container to hold all the movies
 * @param {searchResults} param0
 */
function Movies({ searchResults }) {
  return (
    <div className="movies">
      {searchResults !== undefined && searchResults !== []
        ? searchResults.map((m) => <MovieCard key={m.imdbID} movie={m} />)
        : null}
    </div>
  );
}


/**
 * Search bar
 * @param {{string, function, function}} props
 */
function Search({ value, setValue, onSearch }) {
  return (
    <div className="search">
      <input
        type="text"
        placeholder="Movie name..."
        value={value}
        onChange={(e) => setValue(e.currentTarget.value)}
      />
      <button onClick={onSearch}>Search</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In the sample React application outlined above, HTTP request (the "expensive operation") containing the search string (title of the movie) to the OMDb API are made when the user clicks the "Search" button. The API responds with a list of movies in JSON.

Note: The example above does NOT implement the Debounce pattern.

Not Debounce

Since the "expensive operation" in the example React application above only executes the HTTP request (i.e. "searches for movies") when the "Search" button within the <Search /> component is clicked - Debouncing would have little or no effect on the application's performance.

But that's not how most people use modern web applications.

We are used to web-apps responding immediately as we enter text with our search results (e.g. google). So what happens if we refactor the code to work in that fashion?

Dynamically Searching

Well the most straight-forward approach would be to listen to the onChange event for the <Search /> component, and re-execute the HTTP request (the search) every-time the text changes.

That means that if you were to search "Terminator", the onChange event would get called for each character in the string. Assuming it was typed with no typos, this would create at least 9 get HTTP requests:

  1. "t"
  2. "te"
  3. "ter"
  4. "term"
  5. "termi"
  6. "termina"
  7. "terminat"
  8. "terminato"
  9. "terminator"

That's 9 or more HTTP requests that may be re-executed so rapidly that the first request has not been responded to - not to mention processed, and rendered - before the next request is made.

Expensive Operations

HTTP request are referred to as "expensive" operations because they involve creating a request, encoding the request, transmitting the request over the web, an API receiving the request, then the process repeats in reverse as the request is processed by the API and returned to the source (our React application).

To make things worse, in our example, each HTTP response must be processed and mapped to components (<Movies /> and <MovieCard />) to display the movie information.

Since each <MovieCard /> component has an image of the movie, each of this card's will then have to create another HTTP request to another resource to retrieve the image.

Alternatively, we could keep execution of the search as it was originally, only initiating the get request, when the <Search /> component's click event is triggered.

Problem solved?

Sure, for this simple example - however what happens when you add filtering:

Every movie returned from the OMDb API has Poster,Title,Type,Year, and imdbID properties. Realistically, we might want to filter the returned results by Year, or Type.

For simplicity, let's just explore filtering by Year.

We can create a <YearFilter /> component that will take in the search results as a prop, and then we can use a .reduce() function to get all the years of the movies being rendered:

  // use `reduce()` to get all the different years
  const years = searchResults.reduce((acc, movie) => {
    if(!acc.includes(movie.Year)) {
      acc = [...acc, movie.Year] 
    }
    return acc 
  },[]);
Enter fullscreen mode Exit fullscreen mode

Next we would need create a select, and map all the different years in <option> elements within that <select>.

// map the different years to
{<select> 
{years.map((year) => {
  return <option>{year}</option>
}}) 
}
Enter fullscreen mode Exit fullscreen mode

Combine these two functions, and we should have a <YearFilter> component that displays the years of the movies returned by the search.

It might look something like:

// imports 
import React from 'react' 

/**
 * Component for filtering the movies 
 * @param {{searchResults}} props 
 */
export const YearFilter = ({ searchResults }) =>  {

  // no filter if 
  if(searchResults && searchResults.length < 1) return null

  // get all the different years
  const years = searchResults.reduce((acc, movie) => {
    if(!acc.includes(movie.Year)) {
      acc = [...acc, movie.Year] 
    }
    return acc 
  },[]);


  // map the different years to
  const options = years.map((year) => {
    return <option>{year}</option>;
  });

  // return JSX 
  return (
    <div className="yearFilter">
      <label>Year</label>
      <select>{options}</select>
    </div>
    )  
}

export default YearFilter
Enter fullscreen mode Exit fullscreen mode

Next we would monitor for the <select>'s onChange event, and filter out all the displayed movies to only those that match the result.

I hope at this point you're getting the idea. To keep this article from turning into a tutorial, I'll pause on the example.

The Problem we are solving is that we have a scenario in which we our React application has an expensive operation that is being re-executed rapidly, so rapidly that operation ("effect") may not even finish its execution before another call to the function "effect" is called.

Introducing Debounce

With Debounce, we tell React to only re-execute the query after a certain amount of time. The simplest way to implement this would be to leverage the native setTimeout() function provided by JavaScript, and wrap the timeout around the "expensive operation".

So let's focus just on the operation we are concerned about: retrieving movie titles. Logically, we may want to wait to make the request until someone has stopped typing, or once all the filters have been selected.

Since the OMDb API's free tier only allows 1,000 request per day, we also may want to limit how many requests are made for that reason as well.

So here I've simplified the expensive operation we want to Debounce inside a useEffect hook:

useEffect(() => {
  // using Axios for simplicity 
  Axios.get(baseUrl, { params: {
    apiKey: 'YOUR-API-KEY', s: searchTitle
  } }).then(response => setResults(response.Search))
}, [searchTitle])
Enter fullscreen mode Exit fullscreen mode

Now let's wrap our effect with a setTimeout() ensuring that the effect will only re-execute after a delay.

useEffect(() => {
  // capture the timeout 
  const timeout = setTimeout(() => {
    Axios.get(baseUrl, { params: {
      apiKey: 'YOUR-API-KEY', 
      s: searchTitle 
      } }).then(response => setResults(response.Search))
  }, 400) // timeout of 250 milliseconds 

  // clear the timeout 
  return () => clearTimeout(timeout)
}, [searchTitle])
Enter fullscreen mode Exit fullscreen mode

The setTimeout() function wrapped around the HTTP request to our API in this example now ensures that no matter how many times the effect is called (i.e. anytime the searchTitle changes), the actual network request cannot be called more frequently than in intervals of 400 milliseconds.

The time is an arbitrary number, adjust as needed

Keeping it "DRY"

In all most real-world React applications, there isn't just a single network request. Well, "copy and paste" is never a good option in software development. If we simply copied the effect above and changed the function wrapped inside, we make the first programming mistake of repeating ourselves, and assume technical debt that could be problematic later.

Rather than "copy and pasting" and modifying to suit unique needs, we can abstract the behavior.

In React, we can abstract this functionality using a custom hook.

// useDebounce.js 
import { useEffect, useCallback } from 'react' 

export const useDebounce(effect, dependencies, delay) => {
  // store the provided effect in a `useCallback` hook to avoid 
  // having the callback function execute on each render 
  const callback = useCallback(effect, dependencies)

  // wrap our callback function in a `setTimeout` function 
  // and clear the tim out when completed 
  useEffect(() => {
    const timeout = setTimeout(callback, delay)
    return () => clearTimeout(timeout)
  }, 
  // re-execute  the effect if the delay or callback changes
  [callback, delay]
  )  
}

export default useDebounce 
Enter fullscreen mode Exit fullscreen mode

Now anywhere there's an expensive operation that has potential to be executed often and/rapidly, we simply wrap that function ("effect") within the custom useDebounce hook:

useDebounce(() => {
  // effect 
  Axios.get(baseUrl, { params: {
      apiKey: 'YOUR-API-KEY', 
      s: searchTitle 
      } }).then(response => setResults(response.Search))
}, [searchTitle], 400)  // [dependencies, delay]
Enter fullscreen mode Exit fullscreen mode

And that's Debounce, and how you can abstract the behavior of Debounce to re-use that logic (in a maintainable way) throughout your application.

Conclusion

Implementing debounce in react applications can help avoid unnecessary operations and increase performance. By increasing performance, our React application becomes faster, more responsive to user-input, and provides an improved user-experience.

This pattern can even be abstracted to a custom hook so that the pattern is easy to implement throughout your application, but will be most impactful to "expensive operations" or "effects" that are frequently or rapidly re-executed (and it is not necessary to re-execute).

What do you think? Does Debounce make sense to you? Will you use it?

Oldest comments (6)

Collapse
 
shadowtime2000 profile image
shadowtime2000

Thanks! I will use it in the next project of mine that needs it

Collapse
 
jasonnordheim profile image
Jason

Awesome! Goodluck!

Collapse
 
abhishekjain35 profile image
Abhishek Jain

I was looking for something like this for a few days. Great read.

Collapse
 
jasonnordheim profile image
Jason

So glad you it helped you!

Collapse
 
clarity89 profile image
Alex K. • Edited

Nice explanation! I prefer to use react-use. I has useDebonce hook and lots of other hooks for some common functionality, like api alls, etc.

Collapse
 
jasonnordheim profile image
Jason

That's new little library - excited to check it out! Thanks for sharing!