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
-
Use debounce for user input:
// โ Good const debouncedSearch = debounce(search, 300); input.addEventListener('input', debouncedSearch); -
Use memoize for expensive computations:
// โ Good const memoizedFib = memoize(fibonacci); -
Combine patterns appropriately:
// โ Good const optimized = debounceAndMemoize(expensiveAPI, 300); -
Consider cache size:
// โ Good const memoized = memoizeWithLRU(fn, 100);
Common Mistakes
-
Using debounce for throttle scenarios:
// โ Bad - Debounce for scroll const debouncedScroll = debounce(onScroll, 1000); // โ Good - Throttle for scroll const throttledScroll = throttle(onScroll, 1000); -
Not clearing memoization cache:
// โ Bad - Cache grows indefinitely const memoized = memoize(fn); // โ Good - Use LRU or TTL const memoized = memoizeWithLRU(fn, 100); -
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
Related Resources
- Debounce and Throttle - CSS-Tricks
- Memoization - Wikipedia
- LRU Cache - Wikipedia
- Lodash Debounce
- Lodash Memoize
Next Steps
- Learn about Rate Limiting and Throttling
- Explore Stream Processing Basics
- Study Concurrency Patterns in JavaScript
- Practice optimization techniques
- Build performant applications
Comments