React|Optimization & Security

useDebounce vs useThrottle: Optimizing Search Autocomplete and Scroll Events

0
useDebounce vs useThrottle: Optimizing Search Autocomplete and Scroll Events

Chris built an 'Employee Search' feature for the company intranet.

"Wouldn't it be super convenient if it searched in real-time as you type?"

However, just one hour after deployment, the backend developer came running.

"Chris! The DB CPU hit 100%! Is someone launching a DDoS attack?"

It turned out the culprit was Chris's code. While a user typed "Chris", five API requests (C, Ch, Chr, Chri, Chris) were fired at 0.1-second intervals. With just 100 users, the server started screaming.

Processing frequent events like keyboard input, scrolling, or resizing as-is leads to a performance disaster. Today, we will compare Debounce and Throttle, the two core techniques to control this, and implement them as custom hooks.

1. Debounce: "Just do the last one!"

Debounce is a technique that ensures only the last function (or the very first) is executed among a series of calls.

  • Analogy: Like an elevator door. Even if it's about to close, if someone gets on, it opens again and waits. Only when no one else is boarding does the door finally close and move.
  • Use Cases: Search autocomplete, window resizing (calculating after resizing finishes).
  • Implementation: useDebounce Hook

    In React, we implement this by resetting a timer whenever the value changes.

    // useDebounce.ts
    import { useState, useEffect } from 'react';
    
    // value: The value to watch (e.g., search keyword)
    // delay: Time to wait (ms)
    export function useDebounce<T>(value: T, delay: number): T {
      const [debouncedValue, setDebouncedValue] = useState(value);
    
      useEffect(() => {
        // Update value after the delay time has passed.
        const handler = setTimeout(() => {
          setDebouncedValue(value);
        }, delay);
    
        // What if value changes again before the delay passes?
        // Cancel (clear) the previous timer and start a new one. (This is the key!)
        return () => {
          clearTimeout(handler);
        };
      }, [value, delay]);
    
      return debouncedValue;
    }

    Usage

    // SearchBar.tsx
    function SearchBar() {
      const [keyword, setKeyword] = useState('');
      // The value only changes 0.5 seconds after the user stops typing.
      const debouncedKeyword = useDebounce(keyword, 500);
    
      // useEffect runs only when debouncedKeyword changes (i.e., after 500ms).
      useEffect(() => {
        if (debouncedKeyword) {
          console.log('API Request:', debouncedKeyword);
        }
      }, [debouncedKeyword]);
    
      return <input onChange={(e) => setKeyword(e.target.value)} />;
    }

    2. Throttle: "Keep a steady pace!"

    Throttle restricts events so they fire only once per specified interval (e.g., once every 1 second).

  • Analogy: Like a dripping faucet. No matter how wide you open the tap, the water droplets fall at a constant interval—drip, drip.
  • Use Cases: Scroll events (infinite scroll detection), window resizing (UI rearrangement), drag and drop.
  • Implementation: useThrottle Hook

    Throttle needs to use useRef to remember the "last executed time."

    // useThrottle.ts
    import { useRef, useCallback } from 'react';
    
    export function useThrottle<T extends (...args: any[]) => void>(
      callback: T,
      delay: number
    ) {
      const lastRun = useRef(Date.now());
    
      return useCallback(
        (...args: Parameters<T>) => {
          const timeElapsed = Date.now() - lastRun.current;
    
          // Allow execution only if the specified time (delay) has passed
          if (timeElapsed >= delay) {
            callback(...args);
            lastRun.current = Date.now(); // Update time
          }
        },
        [callback, delay]
      );
    }

    Usage

    // ScrollComponent.tsx
    function ScrollComponent() {
      const handleScroll = () => {
        console.log('Calculating scroll position...');
      };
    
      // Even if scrolled rapidly, it executes only once every 0.2 seconds.
      const throttledScroll = useThrottle(handleScroll, 200);
    
      return (
        <div onScroll={throttledScroll} style={{ height: '300px', overflow: 'scroll' }}>
          {/* Content */}
        </div>
      );
    }

    3. Debounce vs Throttle: Which Should I Use?

    The choice depends clearly on the situation.

    SituationRecommendedReason
    Search InputDebounceSearching while typing is meaningless. Request should be sent after input is finished.
    Infinite ScrollThrottleWe need to check the position during the scroll to know if we hit the bottom.
    Window ResizeThrottleThe layout should change in real-time while resizing to look natural.
    Prevent Double ClickDebounceAccept only the last click (or Leading Edge), or use Throttle to space out clicks.

    Key Takeaways

  • Debounce: "Wait and do it once at the end." Groups sequential events to save processing costs. (Saves API costs)
  • Throttle: "Do it steadily at intervals." Reduces event frequency but ensures intermediate steps run periodically. (Saves browser computation costs)
  • Libraries: While you can implement them yourself, using debounce and throttle functions from Lodash is common in production. However, using React hook versions (like use-debounce) is the more modern approach.

  • We've reduced the load on the server and browser through event optimization. But re-calculating heavy logic or recreating functions inside a component on every render is also a waste of performance.

    It's time to properly use React's memoization trio (useMemo, useCallback, React.memo) to prevent this waste.

    Continuing in: “useMemo, useCallback, React.memo: Memoization Is Not Free”

    🔗 References

  • Lodash Debounce
  • Lodash Throttle
  • CSS Tricks - Debouncing and Throttling Explained
  • More in Optimization & Security

    No other posts in this part.