Problem Statement
Explain debouncing, throttling, and memoization. When would you use each? Provide implementation and real-world examples.
Explanation
Debouncing, throttling, and memoization are three critical optimization techniques that solve different performance problems.
Debouncing:
Delays function execution until after events stop firing. Resets timer on each event. Function only executes after specified quiet period. Perfect for search inputs, form validation, window resize. Reduces API calls dramatically. Implementation uses setTimeout and clearTimeout.
Throttling:
Limits function execution to once per time interval. Guarantees function runs at regular intervals. Does not wait for quiet period like debounce. Perfect for scroll events, mouse movement, animations, infinite scroll. Ensures consistent execution rate. Implementation uses timestamp or flag checking.
Memoization:
Caches function results based on inputs. Returns cached value for repeated calls with same arguments. Only works with pure functions. Perfect for expensive calculations, recursive algorithms, API calls. Trades memory for speed. Implementation uses Map or object as cache.
Key differences:
Debounce waits for pause in events. Throttle limits frequency during continuous events. Memoization caches results, not execution timing. Debounce may never execute if events keep firing. Throttle guarantees execution. Memoization guarantees same output for same input.
When to use debouncing:
Search autocomplete, wait for user to stop typing. Form field validation after user finishes. Window resize handlers. Autosave after editing stops. Any scenario where you want to wait for user to finish action.
When to use throttling:
Scroll event handlers updating UI. Mouse position tracking. Animation frame limiting. Infinite scroll loading. Button click rate limiting. Any scenario where you need regular updates during continuous events.
When to use memoization:
Expensive calculations called repeatedly. Recursive algorithms like Fibonacci. Complex data transformations. API calls with same parameters. Pure function results. Any scenario where calculation is expensive and repeated.
Implementation considerations:
Debounce and throttle need cleanup. Memoization needs memory management. Consider cache size limits. Use WeakMap for object keys when appropriate. Clear caches periodically if needed.
Real-world performance impact:
Debouncing search can reduce API calls from hundreds to one. Throttling scroll can reduce handler calls from thousands to tens. Memoization can turn O(2^n) Fibonacci into O(n). Proper use can make applications 10-100x more performant.
All three techniques are fundamental to building performant web applications and frequently appear in technical interviews.
Code Solution
SolutionRead Only
// DEBOUNCE IMPLEMENTATION
function debounce(func, delay) {
let timeoutId;
return function debounced(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Debounce with immediate execution option
function debounceAdvanced(func, delay, immediate = false) {
let timeoutId;
return function(...args) {
const callNow = immediate && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (!immediate) {
func.apply(this, args);
}
}, delay);
if (callNow) {
func.apply(this, args);
}
};
}
// THROTTLE IMPLEMENTATION
function throttle(func, limit) {
let inThrottle;
return function throttled(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Throttle with leading and trailing options
function throttleAdvanced(func, limit, options = {}) {
let timeout;
let previous = 0;
const {leading = true, trailing = true} = options;
return function(...args) {
const now = Date.now();
if (!previous && !leading) {
previous = now;
}
const remaining = limit - (now - previous);
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(this, args);
} else if (!timeout && trailing) {
timeout = setTimeout(() => {
previous = leading ? Date.now() : 0;
timeout = null;
func.apply(this, args);
}, remaining);
}
};
}
// MEMOIZATION IMPLEMENTATION
function memoize(fn) {
const cache = new Map();
return function memoized(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Memoization with cache limit
function memoizeWithLimit(fn, limit = 100) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
if (cache.size >= limit) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
cache.set(key, result);
return result;
};
}
// REAL-WORLD EXAMPLES
// 1. DEBOUNCE - Search autocomplete
const searchInput = document.getElementById('search');
const searchAPI = async (query) => {
const response = await fetch(`/api/search?q=${query}`);
const results = await response.json();
displayResults(results);
};
const debouncedSearch = debounce(searchAPI, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// User types: a-b-c-d-e
// Without debounce: 5 API calls
// With debounce: 1 API call (after user stops)
// 2. THROTTLE - Infinite scroll
const loadMore = throttle(async () => {
const scrollPos = window.scrollY + window.innerHeight;
const threshold = document.body.scrollHeight - 500;
if (scrollPos >= threshold) {
const data = await fetch('/api/more');
appendItems(await data.json());
}
}, 200);
window.addEventListener('scroll', loadMore);
// User scrolls continuously
// Without throttle: hundreds of calls
// With throttle: max 5 calls per second
// 3. MEMOIZATION - Fibonacci
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(40)); // Fast!
// Without memoization: billions of calculations
// With memoization: 40 calculations
// COMBINED USAGE
class SearchComponent {
constructor() {
// Debounce API calls
this.search = debounce(this.searchAPI.bind(this), 300);
// Throttle UI updates
this.updateUI = throttle(this.render.bind(this), 100);
// Memoize expensive calculations
this.processResults = memoize(this.process.bind(this));
}
async searchAPI(query) {
const results = await fetch(`/api/search?q=${query}`);
return results.json();
}
process(data) {
// Expensive data transformation
return data.map(item => complexCalculation(item));
}
render(data) {
// Update DOM
}
}
// PERFORMANCE COMPARISON
// Example: 1000 scroll events in 1 second
// No optimization:
// 1000 function calls
// 1000 DOM updates
// Very slow, janky scrolling
// With throttle (100ms):
// 10 function calls
// 10 DOM updates
// Smooth scrolling
// Example: User types "javascript" (10 characters)
// No optimization:
// 10 API calls
// Expensive, server load
// With debounce (300ms):
// 1 API call
// Efficient, better UX
// Example: Recursive Fibonacci(40)
// No memoization:
// 2,692,537,104 function calls
// Takes several seconds
// With memoization:
// 40 function calls
// Instant resultPractice Sets
This question appears in the following practice sets: