JavaScript
Completed
-
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)
136: Debouncing and Throttling
I was working on a search feature yesterday, and I noticed something frustrating. Every single keystroke the user typed was triggering a fetch request to the backend. If someone typed "JavaScript", that's 10 API calls in about two seconds. It's a waste of server resources, and if the responses come back out of order, the UI starts flickering like crazy.
The API Spam Problem
Let's look at what happens when we just attach a listener to an input field. I'll write a simple function that simulates a database lookup.
const searchInput = document.querySelector('#search');
function performSearch(query) {
console.log(`Searching for: ${query}...`);
// Imagine a fetch() call here
}
searchInput.addEventListener('input', (e) => {
performSearch(e.target.value);
});
If you try this, you'll see the console explode. Every time you hit a key, the function runs. Now, I don't actually care what the user is searching for until they've stopped typing for a moment. I want to wait for a pause.
Waiting for the Silence
My first instinct is to use a timer. I'll wrap the search call in a setTimeout. I figure, "Okay, wait 300ms before searching."
searchInput.addEventListener('input', (e) => {
setTimeout(() => {
performSearch(e.target.value);
}, 300);
});
But wait—this didn't actually fix the number of calls. It just delayed them. If I type "JavaScript", I still get 10 logs; they just all happen 300ms later. This is where the "debounce" pattern comes in. The trick is to cancel the previous timer every time a new keystroke happens.
let timer;
searchInput.addEventListener('input', (e) => {
// Clear the previous timer if it's still running
clearTimeout(timer);
// Start a new timer
timer = setTimeout(() => {
performSearch(e.target.value);
}, 300);
});
Now we're talking. If I type quickly, the timer keeps getting reset. The search only actually triggers once I stop typing for at least 300ms. That's debouncing: grouping a burst of events into a single execution.
The Jank of the Scroll
Now, let's try to apply that same logic to a different problem. Imagine I'm building a "progress bar" that fills up as the user scrolls down a long page. I'll use the same debounce logic to keep the performance high.
let timer;
window.addEventListener('scroll', () => {
clearTimeout(timer);
timer = setTimeout(() => {
console.log('Updating progress bar...');
// Update DOM here
}, 300);
});
This feels... wrong. If I scroll continuously, the progress bar doesn't move at all until I completely stop scrolling. It's janky. In this case, I don't want to wait for the "silence" at the end; I want regular updates while the action is happening, just not 60 times per second.
Keeping a Steady Beat
This is where we need throttling. Instead of resetting a timer, I need a way to say, "I've executed this function; now ignore all requests for the next 300ms, regardless of how many times the user scrolls."
I'll use a boolean flag to keep track of whether we are currently in a "waiting" period.
let isWaiting = false;
window.addEventListener('scroll', () => {
if (isWaiting) return; // Skip this call if we're still waiting
console.log('Updating progress bar...');
isWaiting = true;
setTimeout(() => {
isWaiting = false; // Open the gate again after 300ms
}, 300);
});
Now, if I scroll for five seconds, the progress bar updates consistently every 300ms. It's smooth enough for the eye, but it doesn't choke the main thread. To sum up the difference in my head: Debouncing is like a door that won't open until people stop crowding it; Throttling is like a turnstile that only lets one person through every few seconds.
📋 Practical Task
Build a Throttled Window Dimension Tracker
Create a webpage that displays the current window width and height in a <div>. Attach an event listener to the resize event of the window to update these numbers. However, window resize events fire incredibly fast. Your task is to implement a throttle function (or logic) that ensures the DOM only updates at most once every 200ms, regardless of how fast the user drags the window corner.
There are no comments for now.