-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
228: Implementing a Debounce and Throttle from Scratch
I remember the first time I built a search autocomplete bar. I hooked up the API call to the input event, thinking I was being efficient by providing real-time results. The problem? Every single keystroke triggered a network request. If a user typed "JavaScript" quickly, that was ten requests in under two seconds. My server logs looked like a DDoS attack, and the UI started flickering because the responses were coming back out of order. I needed a way to tell the browser: "Wait until the user has actually stopped typing for a moment before you send the request."
The "Too Many Logs" Problem
Let's recreate that mess. I'll start with a simple input and a function that simulates an API call. If I just attach the function to the event listener, it's chaotic.
const searchInput = document.querySelector('#search');
function searchAPI(query) {
console.log(`Fetching results for: ${query}...`);
}
searchInput.addEventListener('input', (e) => {
searchAPI(e.target.value);
});
If you type "Hello", the console shows five separate logs. That's a waste of resources. I want it to fire once, but only after the user pauses. My first instinct is to use setTimeout.
Trying to Quiet the Noise
I'll wrap the API call in a timeout. I'm thinking, "Okay, just wait 300ms."
searchInput.addEventListener('input', (e) => {
setTimeout(() => {
searchAPI(e.target.value);
}, 300);
});
I try it out, and... it's actually worse. Now, instead of five immediate logs, I get five logs that all fire 300ms after each keystroke. They're just delayed, not prevented. The problem is that every event creates a new timer. To make this work, I need to kill the previous timer before starting a new one.
Building the Debounce
This is where closures come in. I need a way for the function to "remember" the timer ID across different event calls. I'll write a wrapper function that handles the timing logic for me.
function debounce(func, delay) {
let timer;
return (...args) => {
// If a timer is already running, cancel it.
// This effectively resets the clock on every keystroke.
clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, delay);
};
}
const processSearch = debounce((query) => searchAPI(query), 300);
searchInput.addEventListener('input', (e) => {
processSearch(e.target.value);
});
Now, when I type "Hello", the first four timers are destroyed before they ever finish. Only the final timer—the one created after the 'o'—actually completes. It feels smooth. But here's the catch: debounce is great for "waiting for a pause," but it's terrible for things that happen continuously, like scrolling.
The Scroll Stutter
Let's switch gears. Imagine I'm building a progress bar that fills up as you scroll down a long page. If I use the debounce logic here, the progress bar won't move until the user stops scrolling. That looks broken. I don't want to wait for a pause; I just want to limit the execution to, say, once every 100ms.
I'll try to adapt the debounce logic, but instead of clearing the timer, I'll just ignore new calls if a timer is already active.
Implementing the Throttle
I'll use a "lock" (a boolean flag). If the lock is closed, the function doesn't run. Once the function runs, I lock it and set a timer to unlock it after a while.
function throttle(func, limit) {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
func(...args); // Execute the function immediately
inThrottle = true; // Lock the gate
setTimeout(() => {
inThrottle = false; // Unlock the gate after the limit
}, limit);
}
};
}
const updateProgressBar = () => {
console.log('Updating progress bar...');
};
window.addEventListener('scroll', throttle(updateProgressBar, 100));
Now, while I'm scrolling, the "Updating..." log appears at a steady, rhythmic pace. It doesn't matter how fast the browser fires the scroll event; we've capped the execution rate.
To summarize my reasoning: Debounce is for when you want the final state (the end of a burst of events). Throttle is for when you want a consistent stream of updates but don't need every single pixel of movement to trigger a calculation.
📋 Practical Task
Exercise: The High-Performance Window Resizer
You are building a dashboard with a complex SVG chart that needs to be recalculated every time the browser window is resized. Currently, the resize event is firing dozens of times per second, causing the browser to lag and the chart to stutter.
Your task is to implement a custom throttle function from scratch and apply it to a window resize listener. The chart recalculation should happen no more than once every 200ms, regardless of how fast the user drags the window corner.
- Create a function
throttle(fn, limit)that returns a throttled version of the provided function. - Create a dummy function
recalculateChart()that logs "Chart recalculated!" to the console. - Attach the throttled version of
recalculateChartto thewindow'resize' event. - Verify in the console that while you resize the window rapidly, the logs appear at a steady interval rather than a flood.
There are no comments for now.