Not properly handling these scroll events can lead to serious performance issues:
- Partially by blocking the DOM rendering process.
- A huge number of registered events increases CPU usage, causing reduced battery life of mobile devices.
- If you rely on scroll events for heavier computations, you will inevitably cause memory leaks to occur and degrade your web app further
The practices introduced in this post are especially relevant for heavily interactive websites. They'll help you make your scroll events more efficient in a central space. Let's dive in.
TL:DR
You can find the Code Sandbox for this article here
Use a dedicated scroll entity
While not directly improving our app's performance directly, it's a good idea to collect all scroll events as part of a single function or class. It makes management and debugging of events easier and the code more readable.
function scrollHandler(fns) {
window.addEventListener('scroll', () => {
fns.forEach(fn => fn())
})
}
scrollHandler([
() => {
console.log('hello')
},
() => {
console.log('world')
}
])
The browser will still run all functions at every scroll event so there's still some optimization ahead of us.
Use a queue
When a scroll event's callback function is fired, the browser always waits for all functions to be executed. Collecting all event handlers in a single entity, therefore, introduces a potentially huge performance degradation. Fortunately, we can mitigate it by using a queue.
Note that we're using an array as a queue structure, which might not be the most efficient choice. Check out this article on how to implement your own queue structure in Javascript
function scrollHandler(fns) {
window.addEventListener('scroll', () => {
const queue = [...fns]
function next() {
const fn = queue.shift()
if (fn) {
fn()
requestAnimationFrame(next)
}
}
next()
})
}
By using requestAnimationFrame
, we give the browser time to handle other tasks before the next function in the queue is called. However, the queue still does not solve the problem of every event being fired every time a user scrolls. There are two ways of handling this matter.
Delay (throttle) scroll event
Instead of calling every function on every scroll, providing a fixed rate per second is better. This is what throttling is about.
We can build on our previous two approaches and implement a throttle feature that listens to scroll events every time but only executes them once every 200ms.
function scrollHandlerThrottle(fns) {
let scrolling = false;
window.addEventListener('scroll', () => {
if (!scrolling) {
scrolling = true;
const queue = [...fns];
function next() {
const fn = queue.shift();
if (fn) {
fn();
requestAnimationFrame(next);
}
}
next();
setTimeout(() => {
scrolling = false;
}, 200);
}
})
}
The maximum amount of function calls is therefore limited to 5 per second. You can adjust the limit by changing the second argument of setTimeout
.
Throttling in combination with efficient queue structures is a great way to mitigate the browser's blocking process. There's an alternative approach, however, if you would like to limit function execution even more.
Await (debounce) scroll events
An alternative approach to throttling is debouncing. In this context, this means waiting till the user finishes scrolling. While less interactive compared to throttling, it's a great alternative and, depending on the use case, also more efficient.
All we have to do is implement a timeout that is called 200ms after the initial scroll event is registered.
function scrollHandlerDevounce(fns) {
let scrollTimeout = null;
window.addEventListener('scroll', () => {
if (scrollTimeout) {
clearInterval(scrollTimeout);
}
scrollTimeout = setTimeout(() => {
const queue = [...fns];
function next() {
const fn = queue.shift();
if (fn) {
fn();
requestAnimationFrame(next);
}
}
next();
}, 200);
});
}
Whenever a user scrolls, the event will now only be fired when the scrolling stops for a significant amount of time.
You can even go a step further and combine these methods with other browser features, say the Observer API, to capture your user's interest or execute asynchronous operations.
Top comments (2)
Wow this looks interesting, I didn't know about this. How did you learn this? Genuinely interested
Hi Jose, thanks for your reply and your interest.
I learned about these techniques when working on my OS project, a Ghost theme named Candidus. I had an issue to which these techniques were the solution. Some research (and some help by Perplexity) eventually led me to the code you see now.
That's how I learned quite a bit. The more complex the project is, the more you learn from it.