Throttling Scroll Events in JavaScript

In Brief
Scroll handlers can run many times while the user is still moving the page. Keep the listener cheap, then throttle the expensive work inside it, especially layout reads, DOM writes, analytics calls, animation triggers, and visibility checks. The aim is not to ignore scroll, but to stop one interaction from becoming a stream of unnecessary work.
When you're working on a modern website or web application, you often tend to be working with a lot of moving parts. Things might need to animate on scroll, or you might need to detect when something is visible on screen and have it fade in (although using the Intersection Observer API would be far more performant for this). There are all kinds of reasons to attach a listener to the scroll event, but it can be a good idea to 'throttle' these functions.
One of the key reasons to throttle these functions (and listeners) is to avoid performance degradation; unlike some other events, onScroll tends to trigger very frequently which leads to lower performance, especially if you process a lot of data within that event, causing the interface to slow down or feel laggy. It also forces an increase in client‑side resource use (i.e., the CPU and/or GPU), which can lead to even more lagging.
I'm sure you've arrived on websites in the past where your computer's fans whir into life as soon as things start to load. YouTube used to be extremely bad for this (although more due to general non‑performant code rather than scroll events in particular).
Another reason to throttle or regulate how often your functions run is a little less back‑end focused, but no less important. If you've got animations being triggered on scroll, you can run into a fair few weird visual glitches if you're running them without regulation. Things can look unfinished, broken, too quick, or just unpleasant if you don't slow them down long enough to seem intentional. You can avoid ‑ or at the very least mitigate ‑ these issues by preventing your functions from running, or your listeners from checking, every single millisecond. This is called 'throttling'. So, how do we throttle our scroll events?
Building a DIY Throttle Function
First things first, what do we need our throttling function to do?
To effectively throttle a function, we need to be able to check when it was previously run, then check that a certain amount of time (which we need to be able to have control over) has passed since that last run, and then run the function again if that amount of time has passed.
That all sounds relatively straightforward, so let's whip something up to handle this for us:
// Set up the throttler const throttle = (fn, delay) => { // Capture the current time let time = Date.now(); // Here's our logic return () => { if((time + delay - Date.now()) <= 0) { // Run the function we've passed to our throttler, // and reset the `time` variable (so we can check again). fn(); time = Date.now(); } } } // Here's a dummy function that we want to throttle function runOnScroll(){ console.log('function fired!'); } // We can use this like so (this will run runOnScroll at most once per second): window.addEventListener('scroll', throttle(runOnScroll, 1000)); And that's it ‑ our DIY throttling function is done and ‑ without coincidence ‑ this is similar to how the Lodash throttle function works.
The small throttle function above is enough to explain the basic idea, and may be enough for simple cases. In production code, you should also think about edge cases such as leading calls, trailing calls, cancellation, and how the function should behave when scroll events keep firing continuously. A utility library can be useful if you need those behaviours handled consistently.
Using a Utility Library
Lodash is one common option for this. Its throttle function supports behaviour that the simple example above does not cover, including more control over when the throttled function runs. That does not mean every project needs Lodash. If you only need a simple throttle and already understand the trade‑offs, a small local helper may be enough. If you already use Lodash, or need its fuller throttle behaviour, using its implementation can be a reasonable choice.
If Lodash is already part of your project, or you have decided that its throttle behaviour is worth the dependency, then usage is straightforward:
import { throttle } from 'lodash';const runOnScroll = () => { console.log('function fired!');}window.addEventListener('scroll', throttle(runOnScroll, 250));If you take a look at the throttle documentation, you'll see that all this is doing is saying: when the user is scrolling, trigger the 'runOnScroll' function at most once every 250 milliseconds.
The Wrap‑Up
Function throttling helps prevent scroll handlers from doing unnecessary work while the user is still moving through the page. A small custom throttle can be enough for simple behaviour, while a utility implementation such as Lodash's throttle can be useful when you need more complete handling of edge cases. The important part isn't in the specific implementation, but the decision to keep scroll‑driven work controlled, predictable, and cheap.