Implement a throttle function that limits the rate at which a callback can execute, ensuring it runs at most once per specified time interval.
Throttling is a technique used to control the rate at which a function executes. Unlike debouncing (which delays execution until activity stops), throttling ensures a function executes at regular intervals during continuous activity.
- Scroll Events: Update UI elements (like progress bars or lazy-loading) at a controlled rate while scrolling
- Mouse Movement: Track cursor position without overwhelming the browser with updates
- Window Resize: Recalculate layout at regular intervals during resize
- API Rate Limiting: Ensure requests don't exceed API rate limits
- Game Development: Limit action frequency (e.g., shooting, jumping) to prevent spam
- Auto-save: Save user input at regular intervals while they're typing
- Infinite Scroll: Load more content at controlled intervals while scrolling
function updateScrollPosition(position) {
console.log(`Scroll position: ${position}px`);
}
const throttledUpdate = throttle(updateScrollPosition, 1000);
// User scrolls continuously
throttledUpdate(100); // t=0ms
throttledUpdate(200); // t=100ms
throttledUpdate(300); // t=200ms
throttledUpdate(400); // t=300ms
throttledUpdate(500); // t=1100ms
throttledUpdate(600); // t=1200ms// Executes at regular 1000ms intervals
Scroll position: 100px // t=0ms (immediate)
Scroll position: 500px // t=1100ms (after 1000ms)
Scroll position: 600px // t=2200ms (after another 1000ms)
- The throttle function should accept a function and a time limit
- It should return a new function that limits execution rate
- The function should execute immediately on the first call (leading edge)
- Subsequent calls within the time limit should be controlled
- The function should preserve the correct
thiscontext and arguments - Should handle both leading and trailing edge execution options
- Closures: Maintaining state (lastExecuted, timeoutId) across function calls
- Higher-Order Functions: Returning a function from a function
- setTimeout/clearTimeout: Managing asynchronous delays
- Function Context: Using
apply()to preservethisbinding - Timestamps: Using
Date.now()to track execution timing
Executes immediately on first call, then ignores calls for the limit duration:
function throttleLeading(func, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}Executes immediately and also schedules a final execution:
function throttle(func, limit) {
let lastExecuted = 0;
let timeoutId = null;
return function(...args) {
const now = Date.now();
const timeSinceLastExecution = now - lastExecuted;
if (timeSinceLastExecution >= limit) {
lastExecuted = now;
func.apply(this, args);
} else {
clearTimeout(timeoutId);
const remainingTime = limit - timeSinceLastExecution;
timeoutId = setTimeout(() => {
lastExecuted = Date.now();
func.apply(this, args);
}, remainingTime);
}
};
}| Feature | Throttle | Debounce |
|---|---|---|
| Execution Pattern | At regular intervals during activity | Only after activity stops |
| Frequency | Guaranteed execution every X ms | Single execution after silence |
| Use Case | Continuous updates (scroll, resize) | Wait for completion (search input) |
| Example | Update scroll position every 100ms | Search API call 500ms after typing stops |
| Behavior | Executes periodically while active | Executes once when idle |
User Activity: ████████████████████████████████
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
Throttle: ✓ ✓ ✓ ✓
(executes at regular intervals)
Debounce: ✓
(executes only after activity stops)
- Performance: Reduces unnecessary function calls during high-frequency events
- Consistency: Ensures predictable execution intervals
- User Experience: Provides smooth, responsive updates without lag
- Resource Management: Prevents overwhelming the browser or server
- Battery Life: Reduces CPU usage on mobile devices
// ❌ Wrong: Arrow function loses context
function throttle(func, limit) {
return () => func(); // 'this' is lost
}
// ✓ Correct: Use regular function and apply
function throttle(func, limit) {
return function(...args) {
func.apply(this, args); // Preserves 'this'
};
}// ❌ Wrong: Multiple timeouts can stack up
function throttle(func, limit) {
return function() {
setTimeout(() => func(), limit); // Creates new timeout every call
};
}
// ✓ Correct: Clear previous timeout
function throttle(func, limit) {
let timeoutId;
return function() {
clearTimeout(timeoutId); // Clear previous
timeoutId = setTimeout(() => func(), limit);
};
}// ❌ Wrong: Arguments are lost
function throttle(func, limit) {
return function() {
func(); // No arguments passed
};
}
// ✓ Correct: Collect and pass arguments
function throttle(func, limit) {
return function(...args) {
func.apply(this, args); // Pass all arguments
};
}-
"What's the difference between throttle and debounce?"
- Throttle: Regular intervals during activity
- Debounce: Single execution after activity stops
-
"When would you use throttle over debounce?"
- Use throttle for continuous updates (scroll, resize, mouse move)
- Use debounce for completion-based actions (search, form validation)
-
"How would you implement leading vs trailing edge?"
- Leading: Execute immediately, ignore subsequent calls
- Trailing: Schedule execution for end of interval
- Both: Execute immediately and schedule final call
-
"What are the performance benefits?"
- Reduces function calls (e.g., from 1000/sec to 10/sec)
- Prevents browser lag and jank
- Reduces API calls and server load
-
"How do you preserve the
thiscontext?"- Use regular function (not arrow function)
- Use
func.apply(this, args)to call original function
- ✓ Preserves
thiscontext withapply() - ✓ Passes all arguments with rest/spread operators
- ✓ Clears previous timeouts to prevent memory leaks
- ✓ Uses closures correctly to maintain state
- ✓ Handles edge cases (first call, rapid calls, etc.)
function throttle(func, limit, options = {}) {
const { leading = true, trailing = true } = options;
let lastExecuted = 0;
let timeoutId = null;
return function(...args) {
const now = Date.now();
if (!lastExecuted && !leading) {
lastExecuted = now;
}
const remaining = limit - (now - lastExecuted);
if (remaining <= 0) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastExecuted = now;
func.apply(this, args);
} else if (!timeoutId && trailing) {
timeoutId = setTimeout(() => {
lastExecuted = leading ? Date.now() : 0;
timeoutId = null;
func.apply(this, args);
}, remaining);
}
};
}function throttle(func, limit) {
let timeoutId = null;
let lastExecuted = 0;
const throttled = function(...args) {
const now = Date.now();
const remaining = limit - (now - lastExecuted);
if (remaining <= 0) {
lastExecuted = now;
func.apply(this, args);
}
};
// Add cancel method
throttled.cancel = function() {
clearTimeout(timeoutId);
timeoutId = null;
lastExecuted = 0;
};
return throttled;
}// Test basic throttling
const mockFn = jest.fn();
const throttled = throttle(mockFn, 1000);
throttled(); // Should execute
throttled(); // Should be ignored
jest.advanceTimersByTime(1000);
throttled(); // Should execute
expect(mockFn).toHaveBeenCalledTimes(2);- Debounce: Delays execution until activity stops
- Rate Limiting: Restricts number of calls in a time window
- Request Animation Frame: Browser-optimized throttling for animations
- Web Workers: Offload heavy computations to background threads