|
| 1 | +# Throttle Function |
| 2 | + |
| 3 | +## Challenge |
| 4 | +Implement a throttle function that limits the rate at which a callback can execute, ensuring it runs at most once per specified time interval. |
| 5 | + |
| 6 | +## Problem Description |
| 7 | +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. |
| 8 | + |
| 9 | +### Real-World Use Cases |
| 10 | +- **Scroll Events**: Update UI elements (like progress bars or lazy-loading) at a controlled rate while scrolling |
| 11 | +- **Mouse Movement**: Track cursor position without overwhelming the browser with updates |
| 12 | +- **Window Resize**: Recalculate layout at regular intervals during resize |
| 13 | +- **API Rate Limiting**: Ensure requests don't exceed API rate limits |
| 14 | +- **Game Development**: Limit action frequency (e.g., shooting, jumping) to prevent spam |
| 15 | +- **Auto-save**: Save user input at regular intervals while they're typing |
| 16 | +- **Infinite Scroll**: Load more content at controlled intervals while scrolling |
| 17 | + |
| 18 | +## Example |
| 19 | + |
| 20 | +### Input |
| 21 | +```js |
| 22 | +function updateScrollPosition(position) { |
| 23 | + console.log(`Scroll position: ${position}px`); |
| 24 | +} |
| 25 | + |
| 26 | +const throttledUpdate = throttle(updateScrollPosition, 1000); |
| 27 | + |
| 28 | +// User scrolls continuously |
| 29 | +throttledUpdate(100); // t=0ms |
| 30 | +throttledUpdate(200); // t=100ms |
| 31 | +throttledUpdate(300); // t=200ms |
| 32 | +throttledUpdate(400); // t=300ms |
| 33 | +throttledUpdate(500); // t=1100ms |
| 34 | +throttledUpdate(600); // t=1200ms |
| 35 | +``` |
| 36 | + |
| 37 | +### Output |
| 38 | +``` |
| 39 | +// Executes at regular 1000ms intervals |
| 40 | +Scroll position: 100px // t=0ms (immediate) |
| 41 | +Scroll position: 500px // t=1100ms (after 1000ms) |
| 42 | +Scroll position: 600px // t=2200ms (after another 1000ms) |
| 43 | +``` |
| 44 | + |
| 45 | +## Requirements |
| 46 | +1. The throttle function should accept a function and a time limit |
| 47 | +2. It should return a new function that limits execution rate |
| 48 | +3. The function should execute immediately on the first call (leading edge) |
| 49 | +4. Subsequent calls within the time limit should be controlled |
| 50 | +5. The function should preserve the correct `this` context and arguments |
| 51 | +6. Should handle both leading and trailing edge execution options |
| 52 | + |
| 53 | +## Key Concepts |
| 54 | +- **Closures**: Maintaining state (lastExecuted, timeoutId) across function calls |
| 55 | +- **Higher-Order Functions**: Returning a function from a function |
| 56 | +- **setTimeout/clearTimeout**: Managing asynchronous delays |
| 57 | +- **Function Context**: Using `apply()` to preserve `this` binding |
| 58 | +- **Timestamps**: Using `Date.now()` to track execution timing |
| 59 | + |
| 60 | +## Implementation Approaches |
| 61 | + |
| 62 | +### 1. Leading Edge Throttle |
| 63 | +Executes immediately on first call, then ignores calls for the limit duration: |
| 64 | +```js |
| 65 | +function throttleLeading(func, limit) { |
| 66 | + let inThrottle = false; |
| 67 | + |
| 68 | + return function(...args) { |
| 69 | + if (!inThrottle) { |
| 70 | + func.apply(this, args); |
| 71 | + inThrottle = true; |
| 72 | + setTimeout(() => inThrottle = false, limit); |
| 73 | + } |
| 74 | + }; |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +### 2. Leading + Trailing Edge Throttle |
| 79 | +Executes immediately and also schedules a final execution: |
| 80 | +```js |
| 81 | +function throttle(func, limit) { |
| 82 | + let lastExecuted = 0; |
| 83 | + let timeoutId = null; |
| 84 | + |
| 85 | + return function(...args) { |
| 86 | + const now = Date.now(); |
| 87 | + const timeSinceLastExecution = now - lastExecuted; |
| 88 | + |
| 89 | + if (timeSinceLastExecution >= limit) { |
| 90 | + lastExecuted = now; |
| 91 | + func.apply(this, args); |
| 92 | + } else { |
| 93 | + clearTimeout(timeoutId); |
| 94 | + const remainingTime = limit - timeSinceLastExecution; |
| 95 | + timeoutId = setTimeout(() => { |
| 96 | + lastExecuted = Date.now(); |
| 97 | + func.apply(this, args); |
| 98 | + }, remainingTime); |
| 99 | + } |
| 100 | + }; |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +## Throttle vs Debounce |
| 105 | + |
| 106 | +| Feature | Throttle | Debounce | |
| 107 | +|---------|----------|----------| |
| 108 | +| **Execution Pattern** | At regular intervals during activity | Only after activity stops | |
| 109 | +| **Frequency** | Guaranteed execution every X ms | Single execution after silence | |
| 110 | +| **Use Case** | Continuous updates (scroll, resize) | Wait for completion (search input) | |
| 111 | +| **Example** | Update scroll position every 100ms | Search API call 500ms after typing stops | |
| 112 | +| **Behavior** | Executes periodically while active | Executes once when idle | |
| 113 | + |
| 114 | +### Visual Comparison |
| 115 | +``` |
| 116 | +User Activity: ████████████████████████████████ |
| 117 | + ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ |
| 118 | +
|
| 119 | +Throttle: ✓ ✓ ✓ ✓ |
| 120 | + (executes at regular intervals) |
| 121 | +
|
| 122 | +Debounce: ✓ |
| 123 | + (executes only after activity stops) |
| 124 | +``` |
| 125 | + |
| 126 | +## Benefits |
| 127 | +- **Performance**: Reduces unnecessary function calls during high-frequency events |
| 128 | +- **Consistency**: Ensures predictable execution intervals |
| 129 | +- **User Experience**: Provides smooth, responsive updates without lag |
| 130 | +- **Resource Management**: Prevents overwhelming the browser or server |
| 131 | +- **Battery Life**: Reduces CPU usage on mobile devices |
| 132 | + |
| 133 | +## Common Pitfalls |
| 134 | + |
| 135 | +### 1. Losing `this` Context |
| 136 | +```js |
| 137 | +// ❌ Wrong: Arrow function loses context |
| 138 | +function throttle(func, limit) { |
| 139 | + return () => func(); // 'this' is lost |
| 140 | +} |
| 141 | + |
| 142 | +// ✓ Correct: Use regular function and apply |
| 143 | +function throttle(func, limit) { |
| 144 | + return function(...args) { |
| 145 | + func.apply(this, args); // Preserves 'this' |
| 146 | + }; |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +### 2. Not Clearing Previous Timeouts |
| 151 | +```js |
| 152 | +// ❌ Wrong: Multiple timeouts can stack up |
| 153 | +function throttle(func, limit) { |
| 154 | + return function() { |
| 155 | + setTimeout(() => func(), limit); // Creates new timeout every call |
| 156 | + }; |
| 157 | +} |
| 158 | + |
| 159 | +// ✓ Correct: Clear previous timeout |
| 160 | +function throttle(func, limit) { |
| 161 | + let timeoutId; |
| 162 | + return function() { |
| 163 | + clearTimeout(timeoutId); // Clear previous |
| 164 | + timeoutId = setTimeout(() => func(), limit); |
| 165 | + }; |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +### 3. Forgetting to Pass Arguments |
| 170 | +```js |
| 171 | +// ❌ Wrong: Arguments are lost |
| 172 | +function throttle(func, limit) { |
| 173 | + return function() { |
| 174 | + func(); // No arguments passed |
| 175 | + }; |
| 176 | +} |
| 177 | + |
| 178 | +// ✓ Correct: Collect and pass arguments |
| 179 | +function throttle(func, limit) { |
| 180 | + return function(...args) { |
| 181 | + func.apply(this, args); // Pass all arguments |
| 182 | + }; |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +## Interview Tips |
| 187 | + |
| 188 | +### Questions You Might Be Asked |
| 189 | +1. **"What's the difference between throttle and debounce?"** |
| 190 | + - Throttle: Regular intervals during activity |
| 191 | + - Debounce: Single execution after activity stops |
| 192 | + |
| 193 | +2. **"When would you use throttle over debounce?"** |
| 194 | + - Use throttle for continuous updates (scroll, resize, mouse move) |
| 195 | + - Use debounce for completion-based actions (search, form validation) |
| 196 | + |
| 197 | +3. **"How would you implement leading vs trailing edge?"** |
| 198 | + - Leading: Execute immediately, ignore subsequent calls |
| 199 | + - Trailing: Schedule execution for end of interval |
| 200 | + - Both: Execute immediately and schedule final call |
| 201 | + |
| 202 | +4. **"What are the performance benefits?"** |
| 203 | + - Reduces function calls (e.g., from 1000/sec to 10/sec) |
| 204 | + - Prevents browser lag and jank |
| 205 | + - Reduces API calls and server load |
| 206 | + |
| 207 | +5. **"How do you preserve the `this` context?"** |
| 208 | + - Use regular function (not arrow function) |
| 209 | + - Use `func.apply(this, args)` to call original function |
| 210 | + |
| 211 | +### Code Review Points |
| 212 | +- ✓ Preserves `this` context with `apply()` |
| 213 | +- ✓ Passes all arguments with rest/spread operators |
| 214 | +- ✓ Clears previous timeouts to prevent memory leaks |
| 215 | +- ✓ Uses closures correctly to maintain state |
| 216 | +- ✓ Handles edge cases (first call, rapid calls, etc.) |
| 217 | + |
| 218 | +## Advanced Variations |
| 219 | + |
| 220 | +### Throttle with Options |
| 221 | +```js |
| 222 | +function throttle(func, limit, options = {}) { |
| 223 | + const { leading = true, trailing = true } = options; |
| 224 | + let lastExecuted = 0; |
| 225 | + let timeoutId = null; |
| 226 | + |
| 227 | + return function(...args) { |
| 228 | + const now = Date.now(); |
| 229 | + |
| 230 | + if (!lastExecuted && !leading) { |
| 231 | + lastExecuted = now; |
| 232 | + } |
| 233 | + |
| 234 | + const remaining = limit - (now - lastExecuted); |
| 235 | + |
| 236 | + if (remaining <= 0) { |
| 237 | + if (timeoutId) { |
| 238 | + clearTimeout(timeoutId); |
| 239 | + timeoutId = null; |
| 240 | + } |
| 241 | + lastExecuted = now; |
| 242 | + func.apply(this, args); |
| 243 | + } else if (!timeoutId && trailing) { |
| 244 | + timeoutId = setTimeout(() => { |
| 245 | + lastExecuted = leading ? Date.now() : 0; |
| 246 | + timeoutId = null; |
| 247 | + func.apply(this, args); |
| 248 | + }, remaining); |
| 249 | + } |
| 250 | + }; |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +### Throttle with Cancel Method |
| 255 | +```js |
| 256 | +function throttle(func, limit) { |
| 257 | + let timeoutId = null; |
| 258 | + let lastExecuted = 0; |
| 259 | + |
| 260 | + const throttled = function(...args) { |
| 261 | + const now = Date.now(); |
| 262 | + const remaining = limit - (now - lastExecuted); |
| 263 | + |
| 264 | + if (remaining <= 0) { |
| 265 | + lastExecuted = now; |
| 266 | + func.apply(this, args); |
| 267 | + } |
| 268 | + }; |
| 269 | + |
| 270 | + // Add cancel method |
| 271 | + throttled.cancel = function() { |
| 272 | + clearTimeout(timeoutId); |
| 273 | + timeoutId = null; |
| 274 | + lastExecuted = 0; |
| 275 | + }; |
| 276 | + |
| 277 | + return throttled; |
| 278 | +} |
| 279 | +``` |
| 280 | + |
| 281 | +## Testing Considerations |
| 282 | +```js |
| 283 | +// Test basic throttling |
| 284 | +const mockFn = jest.fn(); |
| 285 | +const throttled = throttle(mockFn, 1000); |
| 286 | + |
| 287 | +throttled(); // Should execute |
| 288 | +throttled(); // Should be ignored |
| 289 | +jest.advanceTimersByTime(1000); |
| 290 | +throttled(); // Should execute |
| 291 | + |
| 292 | +expect(mockFn).toHaveBeenCalledTimes(2); |
| 293 | +``` |
| 294 | + |
| 295 | +## Related Patterns |
| 296 | +- **Debounce**: Delays execution until activity stops |
| 297 | +- **Rate Limiting**: Restricts number of calls in a time window |
| 298 | +- **Request Animation Frame**: Browser-optimized throttling for animations |
| 299 | +- **Web Workers**: Offload heavy computations to background threads |
| 300 | + |
| 301 | +## Further Reading |
| 302 | +- [MDN: Closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) |
| 303 | +- [Lodash throttle implementation](https://lodash.com/docs/#throttle) |
| 304 | +- [CSS Tricks: Debouncing and Throttling](https://css-tricks.com/debouncing-throttling-explained-examples/) |
0 commit comments