Skip to main content
โšก Calmops

Debouncing and Memoization in JavaScript

Debouncing and Memoization in JavaScript

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);
    
  2. Use memoize for expensive computations:

    // โœ… Good
    const memoizedFib = memoize(fibonacci);
    
  3. Combine patterns appropriately:

    // โœ… Good
    const optimized = debounceAndMemoize(expensiveAPI, 300);
    
  4. Consider cache size:

    // โœ… Good
    const memoized = memoizeWithLRU(fn, 100);
    

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);
    
  2. Not clearing memoization cache:

    // โŒ Bad - Cache grows indefinitely
    const memoized = memoize(fn);
    
    // โœ… Good - Use LRU or TTL
    const memoized = memoizeWithLRU(fn, 100);
    
  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

Comments