DEV Community

Cover image for Master Efficient Window Scroll Event Handling in JavaScript: Best Practices and Tips
tq-bit
tq-bit

Posted on • Edited on • Originally published at blog.q-bit.me

Master Efficient Window Scroll Event Handling in JavaScript: Best Practices and Tips

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')
    }
])


Enter fullscreen mode Exit fullscreen mode

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()
    })
}

Enter fullscreen mode Exit fullscreen mode

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);
        }
    })
}

Enter fullscreen mode Exit fullscreen mode

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);
    });
}

Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
eddsaura profile image
Jose E Saura

Wow this looks interesting, I didn't know about this. How did you learn this? Genuinely interested

Collapse
 
tqbit profile image
tq-bit • Edited

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.