JS Scrolling Performance

I’m building a site for work, and we’re coming with ideas to improve the user’s experience on mobile. The idea came up to build one of those headers that appears when you start to scroll up. I like these, because it frees up part of your already-limited viewing space from having a nav stuck to the top of the screen, but it also means you don’t have to go all the way back to the top of the page if you want to access the site nav.

I looked into how other developers have tackled this problem and made a tangential discovery about how to properly handle scroll() events in JavaScript. It’s a topic that has been widely covered, but I hadn’t really put thought into it before. I thought it would be a good idea, therefore, to record my own takeaways.

The Problem

So what exactly is the deal? You might use jQuery’s $(window).scroll() event and then simply perform your calculations/animations/whatever directly inside of the scroll callback. In theory, this works great…but it has a cost. Just how often does that .scroll() event fire? Short answer: a lot.

It depends on the browser and the interface device (e.g. mouse, finger) you use to initiate the scroll. It could fire as much as possible while your finger pans on your mobile device. It could only fire when your mouse wheel clicks as you turn it. The event may (attempt) to fire hundreds or thousands of times a second. If you’re doing something heavy, like animation calculations, this can cause your site to drag. Nobody likes that.

The Solution

The scroll function doesn’t need to run hundreds of times per second - humans won’t be able to detect changes at such a rapid pace on top of the fact it makes your site chug. So, we still use jQuery’s scroll() but instead of performing heavy processes, we set a Boolean flag instead. Then, we use JavaScript’s setInterval() function to check our flag only a few times a second. Something like:

// a Boolean to determine if the page has scrolled
var scrollFlag = false;

// the time to check for scrolling, in milliseconds
var scrollRefresh = 250;

// the polling function
setInterval(function() {
    if ( scrollFlag ) {
        //reset scrollFlag scrollFlag = false;
        //your processes when the page scrolls (i.e. an animation)
}, scrollRefresh);

// when the window scrolls, simply update the Boolean
$(window).scroll(function(){ scrollFlag = true; });

The notable thing above is the scrollRefresh variable. This is how often we check the scrollFlag variable to see if it’s changed. In the example above, we’re using 250 milliseconds - which means we’re checking to see if we need to do something 4 times a second. You do need to be careful using setInterval() - if you set it to run too frequently you could cause the same performance issues we’re trying to avoid.

How often to check for scrolls

Checking less often is certainly less intensive, but this can result in your site feeling sluggish. So what’s the sweet spot? That will ultimately depend on your implentation and what you’re doing. In my tests, I used 400ms to start with (checking around twice a second) but the nav showed up an instant later than I was expecting. This isn’t a huge issue - the site still works - but users appreciate a site more when things feel fluid.

I started thinking about animation - specifically with respect to gaming. The holy grail is often considered to be 60 frames per second (FPS), so maybe I should check for scrolls 60 times a second. Some quick math told me I’d need to check the variable about every 17 milliseconds. While this certainly felt snappy on my desktop, I was a little concerned about it on a few mobile browsers.

I changed my approach again. Instead of shooting for a specific response rate, I should be trying to perform as few operations as possible while causing the least amount of “discomfort” for the user. Based on my first two tests, I knew I needed to check somewhere between 400ms and 16ms. Since I was trying for a maximum, I started at the 400ms end and started reducing the number until my UI felt responsive on both my phone and my desktop. 250ms ended up being what worked best for me.

I don’t think there’s a magic formula - if there is, please share? - but I would say the goal is to always shoot high, and work your way back. You’ll eventually find the spot that is both performant and slick…and if you don’t maybe you need to rethink your implementation entirely. Less is more is in vogue now for a reason.