Skip to Content
Course content

228: Implementing a Debounce and Throttle from Scratch

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 recalculateChart to the window 'resize' event.
  • Verify in the console that while you resize the window rapidly, the logs appear at a steady interval rather than a flood.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.