← Back to snippets

Debounce Functions in JavaScript

Learn how to implement and use debounce patterns for optimizing frequent events

javascript
beginner

Debounce Functions in JavaScript

When handling frequent events like typing, resizing, or input changes, debounce is an essential technique for improving application performance. Debouncing postpones the execution of a function until after a specified waiting period has elapsed since its last call.

Basic Debounce Implementation

Debouncing ensures that a function won't execute until a certain amount of time passes without it being called again. It's ideal for scenarios where you want to wait for a user to finish an action before responding.

const debounce = (fn, delay) => {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delay);
  };
};

// Usage example
const handleSearch = (query) => {
  console.log(`Searching for: ${query}`);
  // Make API call with query
  fetch(`/api/search?q=${query}`);
};

// Create a debounced version - only searches after user stops typing for 500ms
const debouncedSearch = debounce(handleSearch, 500);

// Event listener for search input
document.getElementById('search').addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

Enhanced Debounce with Immediate Execution

Sometimes you want the function to execute immediately on the first call and then debounce subsequent calls. This variant provides that flexibility:

const debounceWithImmediate = (fn, delay, immediate = false) => {
  let timeoutId;
  return function(...args) {
    const callNow = immediate && !timeoutId;
    
    clearTimeout(timeoutId);
    
    timeoutId = setTimeout(() => {
      timeoutId = null;
      if (!immediate) fn.apply(this, args);
    }, delay);
    
    if (callNow) fn.apply(this, args);
  };
};

// Execute immediately on first call, then debounce
const saveChanges = debounceWithImmediate(() => {
  console.log('Saving changes...');
  // API call to save data
}, 1000, true);

Cancellable Debounce

For cases where you need to manually cancel pending debounced calls, such as when a component unmounts:

const cancellableDebounce = (fn, delay) => {
  let timeoutId;
  
  const debounced = function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delay);
  };
  
  debounced.cancel = () => {
    clearTimeout(timeoutId);
    timeoutId = null;
  };
  
  return debounced;
};

// Usage with cancelation
const saveInput = cancellableDebounce(() => {
  localStorage.setItem('form', JSON.stringify(formData));
}, 800);

// Call normally
saveInput();

// Later, when needed (e.g., component unmount)
saveInput.cancel();

Common Use Cases for Debouncing

  1. Search Inputs
const searchInput = document.getElementById('search');
const resultsDiv = document.getElementById('results');

const performSearch = async (query) => {
  if (!query.trim()) {
    resultsDiv.innerHTML = '';
    return;
  }
  
  resultsDiv.innerHTML = '<div class="loading">Searching...</div>';
  
  try {
    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    const data = await response.json();
    
    resultsDiv.innerHTML = data.results.length 
      ? data.results.map(item => `<div class="result">${item.title}</div>`).join('')
      : '<div class="no-results">No results found</div>';
  } catch (error) {
    resultsDiv.innerHTML = '<div class="error">Search failed</div>';
  }
};

const debouncedSearch = debounce(performSearch, 400);

searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});
  1. Window Resize Events
const handleResize = () => {
  // Calculate new dimensions
  const width = window.innerWidth;
  const height = window.innerHeight;
  
  // Update UI elements based on new size
  document.querySelector('.container').style.width = width < 768 ? '100%' : '80%';
  
  // Perform expensive layout recalculations
  recalculateLayout();
};

const debouncedResize = debounce(handleResize, 250);

window.addEventListener('resize', debouncedResize);
  1. Form Input Validation
const usernameInput = document.getElementById('username');
const feedback = document.getElementById('feedback');

const checkUsername = async (username) => {
  if (username.length < 3) {
    feedback.textContent = 'Username must be at least 3 characters';
    return;
  }
  
  try {
    const response = await fetch(`/api/check-username?username=${username}`);
    const { available } = await response.json();
    
    feedback.textContent = available
      ? '✅ Username is available'
      : '❌ Username is already taken';
  } catch (error) {
    feedback.textContent = 'Error checking username availability';
  }
};

const debouncedCheck = debounce(checkUsername, 500);

usernameInput.addEventListener('input', (e) => {
  feedback.textContent = 'Checking...';
  debouncedCheck(e.target.value);
});

When to Use Debounce vs Throttle

Debounce and throttle are similar techniques but serve different purposes. For more on throttling, see the Throttle Functions in JavaScript snippet.

TechniqueBehaviorBest for
DebounceGroups multiple sequential calls into one after a quiet periodTyping, search inputs, form validation
ThrottleLimits execution to a maximum frequencyScrolling, mouse movements, game loops

Performance Considerations

  • Choose an appropriate delay: Too short negates the benefits, too long affects UX
  • Avoid recreating debounced functions: Create them once, outside of render functions
  • Clean up event listeners: Remove debounced listeners when components unmount
  • Use named functions: Instead of anonymous functions for better debugging

Debouncing is a powerful technique that drastically improves performance when implemented correctly, especially in UI-heavy applications with many event handlers.