Hack Frontend Community

Debounce and Throttle in JavaScript

What are Debounce and Throttle?

Debounce and Throttle are performance optimization techniques that control the frequency of function execution, especially when handling events that can be triggered very frequently (scroll, resize, input, etc.).


Debounce

Debounce delays function execution until a certain amount of time has passed since the last invocation.

When to use?

  • Search with autocomplete (send request after user stops typing)
  • Real-time form validation
  • Draft saving (auto-save after pause in editing)

Implementation Example

function debounce(func, delay) {
  let timeoutId;
  
  return function(...args) {
    // Clear previous timer
    clearTimeout(timeoutId);
    
    // Set new timer
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// Usage
const searchInput = document.querySelector('#search');

const handleSearch = debounce((event) => {
  console.log('Search:', event.target.value);
  // API request
}, 500);

searchInput.addEventListener('input', handleSearch);

How does it work?

  1. On each function call, the previous timer is cleared
  2. A new timer is set
  3. The function executes only if delay milliseconds have passed since the last call

Result:

If user types "javascript", instead of 10 requests (one per letter), only one request will be sent 500ms after user finishes typing.


Throttle

Throttle ensures the function executes no more than once per specified time interval.

When to use?

  • Handling scroll events (infinite scroll, lazy loading)
  • Handling window resize
  • Tracking mouse movement
  • Submit button (protection from multiple clicks)

Implementation Example

function throttle(func, limit) {
  let inThrottle;
  
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// Usage
const handleScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
  // Check if reached end of page
}, 1000);

window.addEventListener('scroll', handleScroll);

How does it work?

  1. On first call, function executes immediately
  2. inThrottle flag is set to true
  3. All subsequent calls are ignored until limit milliseconds pass
  4. After time expires, function can be called again

Result:

If user scrolls page rapidly, function will be called maximum once per second, instead of hundreds of times per second.


Comparing Debounce and Throttle

CharacteristicDebounceThrottle
BehaviorExecutes after pauseExecutes at intervals
First callDelayedExecutes immediately
Frequent callsAll resetExecutes once per N ms
Use casesSearch, auto-saveScroll, resize, tracking

Visualization

Imagine an event occurs 10 times per second:

Without optimization:

█ █ █ █ █ █ █ █ █ █  (10 calls)

With Debounce (500ms):

░ ░ ░ ░ ░ ░ ░ ░ ░ █  (1 call after pause)

With Throttle (500ms):

█ ░ ░ ░ ░ █ ░ ░ ░ ░  (2 calls at intervals)

Advanced Implementation with leading and trailing

Debounce with options

function debounce(func, delay, { leading = false, trailing = true } = {}) {
  let timeoutId;
  
  return function(...args) {
    const isInvokingLater = !timeoutId;
    
    clearTimeout(timeoutId);
    
    if (leading && isInvokingLater) {
      func.apply(this, args);
    }
    
    timeoutId = setTimeout(() => {
      if (trailing) {
        func.apply(this, args);
      }
      timeoutId = null;
    }, delay);
  };
}
  • leading: true — call at the beginning
  • trailing: true — call at the end (default)

Throttle with options

function throttle(func, limit, { leading = true, trailing = true } = {}) {
  let inThrottle;
  let lastArgs;
  
  return function(...args) {
    if (!inThrottle) {
      if (leading) {
        func.apply(this, args);
      }
      inThrottle = true;
      
      setTimeout(() => {
        inThrottle = false;
        if (trailing && lastArgs) {
          func.apply(this, lastArgs);
          lastArgs = null;
        }
      }, limit);
    } else {
      lastArgs = args;
    }
  };
}

Using Libraries

In production, ready-made solutions are often used:

Lodash

import { debounce, throttle } from 'lodash';

const debouncedSearch = debounce(searchFunction, 500);
const throttledScroll = throttle(scrollHandler, 1000);

Canceling execution

const debouncedFn = debounce(someFunction, 1000);

// Cancel pending call
debouncedFn.cancel();

React Example

import { useCallback, useRef, useEffect } from 'react';

function useDebounce(callback, delay) {
  const timeoutRef = useRef(null);
  
  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);
  
  return useCallback((...args) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    
    timeoutRef.current = setTimeout(() => {
      callback(...args);
    }, delay);
  }, [callback, delay]);
}

// Usage in component
function SearchComponent() {
  const handleSearch = useDebounce((value) => {
    console.log('Searching:', value);
  }, 500);
  
  return (
    <input 
      type="text" 
      onChange={(e) => handleSearch(e.target.value)} 
    />
  );
}

Mistakes and Pitfalls

Forgetting about this context

// Wrong
function debounce(func, delay) {
  let timeoutId;
  return function() {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(func, delay); // lose this
  };
}

// Correct
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay); // ✅
  };
}

Creating new debounced function on each render

// Wrong in React
function Component() {
  // Each render creates new function
  const handler = debounce(() => {}, 500);
  
  return <input onChange={handler} />;
}

// Correct
function Component() {
  // Function created once
  const handler = useMemo(
    () => debounce(() => {}, 500),
    []
  );
  
  return <input onChange={handler} />;
}

Conclusion

  • Debounce — delays execution until pause in calls (search, auto-save)
  • Throttle — limits execution frequency (scroll, resize)
  • Both techniques are critical for application performance
  • In production, use ready-made libraries (lodash, underscore)
  • Don't forget about context (this) and arguments in implementation
  • In React, use useCallback or useMemo to preserve functions between renders

In Interviews:

Often asked to implement debounce/throttle from scratch and explain differences. It's important to understand not only implementation but also practical use cases.