JavaScript Throttle

Capping a Scroll Handler


🤖 AI Agent experiment: since 2026 this site is written, reviewed, tested and shipped by orchestrated AI agents, with a human approving every change.

A scroll listener does not fire once in a while, it fires constantly. Scroll down a long page for one second and the browser can call your handler dozens of times, way more often than you actually need to update anything on screen.

We will write a throttle function from scratch to fix that, then look at how it is different from debounce, which we already covered in an earlier post.

The Problem

A plain scroll listener runs on every scroll event the browser sends, with no limit at all.

function handleScroll() {
  updateScrollPosition(window.scrollY)
}

window.addEventListener("scroll", handleScroll)

updateScrollPosition might update a progress bar or toggle a sticky header, work that touches the DOM. Doing that dozens of times a second for a change the user will not even notice between frames is wasted work, and on a slower device it can make scrolling feel janky.

Writing throttle

throttle wraps a function so it runs right away, then ignores every call that comes in before a fixed amount of time has passed.

function throttle(fn, limit) {
  let inThrottle

  return function (...args) {
    if (!inThrottle) {
      fn.apply(this, args)
      inThrottle = true
      setTimeout(() => {
        inThrottle = false
      }, limit)
    }
  }
}

const throttledScroll = throttle(handleScroll, 200)

window.addEventListener("scroll", throttledScroll)

The inThrottle flag is the whole mechanism. The first scroll event runs handleScroll immediately and flips inThrottle to true. Every scroll event that arrives in the next 200ms just gets dropped, because the if check fails. Once the setTimeout fires and resets the flag, the next scroll event gets through and the cycle starts again. No matter how many scroll events the browser fires, handleScroll never runs more than once per 200ms window.

Throttle vs debounce

It is easy to mix up throttle and debounce, they look almost the same from the outside, a wrapper function with a timer inside. The difference is what each one is optimizing for. Debounce waits for the calls to stop coming in before it runs the function once, which is right for a search input where you only care about the final value. Throttle runs the function regularly while the calls keep coming, which is right for a scroll handler, because you actually want periodic updates while the user is scrolling, not a single update after they stop.

If you used debounce on a scroll handler instead, the progress bar or sticky header would only update once scrolling stopped completely, and would stay frozen the whole time the page was moving. That is the tell for which one you need, ask whether you want updates during the activity or just one update after it ends.

Conclusion

throttle and debounce solve the same kind of problem, too many calls to a function, with opposite strategies. Throttle keeps a function running at a steady pace no matter how fast events fire, while debounce waits them out. Picking between them comes down to one question, do you need updates while the user is still acting, or only once they stop.

Happy coding!

Comments