⏱️
Intermediate
9 min read

Debouncing & Throttling

Controlling function execution frequency for better performance

Understanding Debounce and Throttle

Debouncing and throttling are techniques to control how often a function executes, particularly useful for expensive operations triggered by frequent events like scrolling, resizing, or typing.

Debounce

Debounce delays function execution until after a specified time has passed since the last invocation. Perfect for search inputs and auto-save features.

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

// ❌ Bad: Search API called on every keystroke
searchInput.addEventListener('input', (e) => {
  searchAPI(e.target.value); // Called hundreds of times
});

// ✅ Good: Search API called only after user stops typing
const debouncedSearch = debounce((query) => {
  searchAPI(query);
}, 300);

searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

// Advanced debounce with leading edge option
function debounceAdvanced(func, delay, options = {}) {
  let timeoutId;
  let lastCallTime = 0;
  
  return function(...args) {
    const now = Date.now();
    const isLeading = options.leading && (now - lastCallTime > delay);
    
    clearTimeout(timeoutId);
    
    if (isLeading) {
      func.apply(this, args);
      lastCallTime = now;
    }
    
    timeoutId = setTimeout(() => {
      if (!options.leading || !isLeading) {
        func.apply(this, args);
      }
      lastCallTime = Date.now();
    }, delay);
  };
}

Throttle

Throttle ensures a function executes at most once per specified time period. Perfect for scroll events and window resize handlers.

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

// ❌ Bad: Scroll handler called hundreds of times per second
window.addEventListener('scroll', () => {
  updateScrollPosition(); // Called excessively
});

// ✅ Good: Scroll handler called at most every 100ms
const throttledScroll = throttle(() => {
  updateScrollPosition();
}, 100);

window.addEventListener('scroll', throttledScroll);

// Advanced throttle with trailing edge
function throttleAdvanced(func, limit, options = {}) {
  let timeout;
  let previous = 0;
  
  return function(...args) {
    const now = Date.now();
    const remaining = limit - (now - previous);
    
    if (remaining <= 0 || remaining > limit) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func.apply(this, args);
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(() => {
        previous = options.leading === false ? 0 : Date.now();
        timeout = null;
        func.apply(this, args);
      }, remaining);
    }
  };
}

Debounce vs Throttle Comparison

// Visualizing the difference
const events = [0, 50, 100, 150, 200, 250, 300, 350, 400];

// Without debounce/throttle: Function called 9 times
// Output: [0, 50, 100, 150, 200, 250, 300, 350, 400]

// With debounce (100ms): Function called 1 time
// Output: [400] (only after user stops)

// With throttle (100ms): Function called 5 times  
// Output: [0, 100, 200, 300, 400]

Real-World Use Cases

// 1. Auto-save (debounce)
const autoSave = debounce((content) => {
  saveToServer(content);
}, 1000);

editor.addEventListener('input', (e) => {
  autoSave(e.target.value);
});

// 2. Infinite scroll (throttle)
const loadMore = throttle(() => {
  if (isNearBottom()) {
    fetchMoreItems();
  }
}, 200);

window.addEventListener('scroll', loadMore);

// 3. Window resize (debounce)
const handleResize = debounce(() => {
  recalculateLayout();
}, 250);

window.addEventListener('resize', handleResize);

// 4. Button click prevention (throttle)
const submitForm = throttle((formData) => {
  sendToAPI(formData);
}, 2000, { trailing: false });

button.addEventListener('click', () => {
  submitForm(getFormData());
});

// 5. Search with autocomplete (debounce)
const autocomplete = debounce(async (query) => {
  if (query.length < 3) return;
  
  const results = await fetchSuggestions(query);
  displaySuggestions(results);
}, 300);

searchBox.addEventListener('input', (e) => {
  autocomplete(e.target.value);
});

React Implementation

// Custom React hooks for debounce and throttle
import { useEffect, useCallback, useRef } 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]);
}

function useThrottle(callback, limit) {
  const inThrottleRef = useRef(false);
  
  return useCallback((...args) => {
    if (!inThrottleRef.current) {
      callback(...args);
      inThrottleRef.current = true;
      
      setTimeout(() => {
        inThrottleRef.current = false;
      }, limit);
    }
  }, [callback, limit]);
}

// Usage in component
function SearchComponent() {
  const handleSearch = useDebounce((query) => {
    fetchResults(query);
  }, 300);
  
  return (
     handleSearch(e.target.value)}
      placeholder="Search..."
    />
  );
}

When to Use Each

  • Debounce: Search boxes, auto-save, form validation, API calls after typing
  • Throttle: Scroll handlers, window resize, mousemove tracking, animation frame limiting
  • Use debounce when you want the final value after events stop
  • Use throttle when you want consistent updates at a fixed rate
  • Debounce delay: 200-500ms for typing, 250-500ms for resize
  • Throttle limit: 100-200ms for scroll, 16ms for animations (60fps)
  • Always clean up event listeners to prevent memory leaks
  • Consider using requestAnimationFrame for scroll-based animations