Skip to main content

Debouncing and Memoization in JavaScript

Created: May 8, 2026 Larry Qu 6 min read

Debouncing and memoization are powerful optimization techniques. This article covers implementing these patterns and practical applications.

Introduction

Debouncing and memoization:

  • Reduce unnecessary function calls
  • Improve performance
  • Optimize resource usage
  • Enhance user experience
  • Cache expensive computations

Understanding these patterns helps you:

  • Build responsive applications
  • Optimize performance
  • Reduce server load
  • Improve user experience

Debouncing

Basic Debounce

// ✅ Good: Basic debounce function
function debounce(fn, delay) {
  let timeoutId;
  
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

// Usage: Search input
const debouncedSearch = debounce((query) => {
  console.log('Searching for:', query);
  // Make API call
}, 300);

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

Debounce with Immediate Execution

// ✅ Good: Debounce with immediate option
function debounceImmediate(fn, delay, immediate = false) {
  let timeoutId;
  
  return function(...args) {
    const callNow = immediate && !timeoutId;
    
    clearTimeout(timeoutId);
    
    timeoutId = setTimeout(() => {
      timeoutId = null;
      if (!immediate) fn(...args);
    }, delay);
    
    if (callNow) fn(...args);
  };
}

// Usage: Execute immediately, then debounce
const debouncedClick = debounceImmediate(() => {
  console.log('Button clicked');
}, 500, true);

document.getElementById('button').addEventListener('click', debouncedClick);

Debounce with Trailing and Leading

// ✅ Good: Debounce with leading and trailing options
function debounceAdvanced(fn, delay, options = {}) {
  const { leading = false, trailing = true, maxWait = null } = options;
  
  let timeoutId;
  let lastCall = 0;
  let lastResult;
  
  return function(...args) {
    const now = Date.now();
    
    if (leading && !timeoutId && now - lastCall >= delay) {
      lastResult = fn(...args);
      lastCall = now;
    }
    
    clearTimeout(timeoutId);
    
    timeoutId = setTimeout(() => {
      if (trailing) {
        lastResult = fn(...args);
      }
      timeoutId = null;
    }, delay);
    
    return lastResult;
  };
}

// Usage
const debouncedResize = debounceAdvanced(() => {
  console.log('Window resized');
}, 500, { leading: true, trailing: true });

window.addEventListener('resize', debouncedResize);

Practical Debounce Examples

// ✅ Good: Debounce for form validation
const debouncedValidate = debounce((email) => {
  if (email.includes('@')) {
    console.log('Valid email');
  } else {
    console.log('Invalid email');
  }
}, 500);

document.getElementById('email').addEventListener('input', (e) => {
  debouncedValidate(e.target.value);
});
// ✅ Good: Debounce for auto-save
const debouncedSave = debounce((content) => {
  console.log('Saving:', content);
  // Make API call to save
}, 1000);

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

Memoization

Basic Memoization

// ✅ Good: Basic memoization
function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log('From cache:', key);
      return cache.get(key);
    }
    
    console.log('Computing:', key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

// Usage
const memoizedAdd = memoize((a, b) => {
  console.log('Adding:', a, b);
  return a + b;
});

console.log(memoizedAdd(1, 2)); // Computing: [1,2] → 3
console.log(memoizedAdd(1, 2)); // From cache: [1,2] → 3
console.log(memoizedAdd(2, 3)); // Computing: [2,3] → 5

Memoization with TTL

// ✅ Good: Memoization with time-to-live
function memoizeWithTTL(fn, ttl = 60000) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    const cached = cache.get(key);
    
    if (cached && Date.now() - cached.timestamp < ttl) {
      console.log('From cache:', key);
      return cached.value;
    }
    
    console.log('Computing:', key);
    const result = fn(...args);
    cache.set(key, {
      value: result,
      timestamp: Date.now()
    });
    return result;
  };
}

// Usage: Cache for 5 seconds
const memoizedFetch = memoizeWithTTL(async (url) => {
  const response = await fetch(url);
  return response.json();
}, 5000);

Memoization with Size Limit

// ✅ Good: Memoization with LRU cache
class LRUCache {
  constructor(maxSize = 100) {
    this.maxSize = maxSize;
    this.cache = new Map();
  }

  get(key) {
    if (!this.cache.has(key)) return null;
    
    // Move to end (most recently used)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    return value;
  }

  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    
    this.cache.set(key, value);
    
    // Remove oldest if exceeds max size
    if (this.cache.size > this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
  }
}

function memoizeWithLRU(fn, maxSize = 100) {
  const cache = new LRUCache(maxSize);
  
  return function(...args) {
    const key = JSON.stringify(args);
    const cached = cache.get(key);
    
    if (cached !== null) {
      console.log('From cache:', key);
      return cached;
    }
    
    console.log('Computing:', key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

// Usage
const memoizedFib = memoizeWithLRU((n) => {
  if (n <= 1) return n;
  return memoizedFib(n - 1) + memoizedFib(n - 2);
}, 50);

console.log(memoizedFib(10)); // Efficient computation

Async Memoization

// ✅ Good: Memoization for async functions
function memoizeAsync(fn) {
  const cache = new Map();
  const pending = new Map();
  
  return async function(...args) {
    const key = JSON.stringify(args);
    
    // Return cached result
    if (cache.has(key)) {
      console.log('From cache:', key);
      return cache.get(key);
    }
    
    // Return pending promise
    if (pending.has(key)) {
      console.log('Waiting for pending:', key);
      return pending.get(key);
    }
    
    // Compute and cache
    console.log('Computing:', key);
    const promise = fn(...args);
    pending.set(key, promise);
    
    try {
      const result = await promise;
      cache.set(key, result);
      return result;
    } finally {
      pending.delete(key);
    }
  };
}

// Usage
const memoizedFetch = memoizeAsync(async (url) => {
  const response = await fetch(url);
  return response.json();
});

// Multiple calls with same URL only fetch once
Promise.all([
  memoizedFetch('/api/data'),
  memoizedFetch('/api/data'),
  memoizedFetch('/api/data')
]);

Practical Patterns

Debounce + Memoize

// ✅ Good: Combine debounce and memoize
function debounceAndMemoize(fn, delay) {
  const memoized = memoize(fn);
  return debounce(memoized, delay);
}

// Usage: Search with caching
const searchAPI = debounceAndMemoize(async (query) => {
  const response = await fetch(`/api/search?q=${query}`);
  return response.json();
}, 300);

document.getElementById('search').addEventListener('input', (e) => {
  searchAPI(e.target.value).then(results => {
    console.log('Results:', results);
  });
});

Memoize with Dependency Tracking

// ✅ Good: Memoize with dependency tracking
function memoizeWithDeps(fn, getDeps) {
  let lastDeps;
  let lastResult;
  
  return function(...args) {
    const deps = getDeps(...args);
    
    if (lastDeps && JSON.stringify(deps) === JSON.stringify(lastDeps)) {
      console.log('From cache (deps unchanged)');
      return lastResult;
    }
    
    console.log('Computing (deps changed)');
    lastDeps = deps;
    lastResult = fn(...args);
    return lastResult;
  };
}

// Usage: React-like useMemo pattern
const expensiveComputation = memoizeWithDeps(
  (a, b) => {
    console.log('Computing:', a, b);
    return a + b;
  },
  (a, b) => [a, b] // Dependencies
);

console.log(expensiveComputation(1, 2)); // Computing
console.log(expensiveComputation(1, 2)); // From cache
console.log(expensiveComputation(2, 3)); // Computing

Throttle + Memoize

// ✅ Good: Combine throttle and memoize
function throttleAndMemoize(fn, delay) {
  const memoized = memoize(fn);
  return throttle(memoized, delay);
}

// Usage: Scroll event with caching
const throttledScroll = throttleAndMemoize(() => {
  const position = window.scrollY;
  console.log('Scroll position:', position);
}, 1000);

window.addEventListener('scroll', throttledScroll);

Best Practices

  1. Use debounce for user input:
    // ✅ Good
    const debouncedSearch = debounce(search, 300);
    input.addEventListener('input', debouncedSearch);
    ```javascript
    
  2. Use memoize for expensive computations:
    // ✅ Good
    const memoizedFib = memoize(fibonacci);
    ```javascript
    
  3. Combine patterns appropriately:
    // ✅ Good
    const optimized = debounceAndMemoize(expensiveAPI, 300);
    ```javascript
    
  4. Consider cache size:
    // ✅ Good
    const memoized = memoizeWithLRU(fn, 100);
    ```javascript
    

Common Mistakes

  1. Using debounce for throttle scenarios:
    // ❌ Bad - Debounce for scroll
    const debouncedScroll = debounce(onScroll, 1000);
    
    // ✅ Good - Throttle for scroll
    const throttledScroll = throttle(onScroll, 1000);
    ```javascript
    
  2. Not clearing memoization cache:
    // ❌ Bad - Cache grows indefinitely
    const memoized = memoize(fn);
    
    // ✅ Good - Use LRU or TTL
    const memoized = memoizeWithLRU(fn, 100);
    ```javascript
    
  3. Memoizing with side effects:
    // ❌ Bad - Function has side effects
    const memoized = memoize(() => {
      console.log('Side effect');
      return value;
    });
    
    // ✅ Good - Pure function
    const memoized = memoize((a, b) => a + b);
    

Summary

Debouncing and memoization are powerful optimization techniques. Key takeaways:

  • Debounce for delayed execution after events
  • Throttle for regular execution during events
  • Memoize for caching expensive computations
  • Use LRU cache for memory efficiency
  • Combine patterns for optimal performance
  • Consider TTL for time-sensitive data
  • Implement async memoization for API calls

Next Steps

Resources

Comments

Share this article

Scan to read on mobile