loading...

Using requestIdleCallback to create a less janky infinite scroll

ben profile image Ben Halpern ・3 min read

We recently switched from a paginated home feed to one with infinite scroll. We should have done this a while ago, as this makes sitting on the toilet and browsing through the main dev.to feed much easier. And isn't that the ultimate goal?

But appending 30 elements to the page while the user scrolls without interrupting frame rate does not come automatically. I wanted us to do whatever we could to ensure a jank-free experience. This is why I made use of requestIdleCallback.

The requestIdleCallback API is a newish API with limited browser support, but it is really easy to work with, and supports the goal we were trying to achieve. The API will schedule work when there is free time at the end of a frame, or when the user is inactive. Infinite scroll is a good use case for this behavior. It is a situation that involves heavy browser repainting at the same time as scrolling.

Scheduling non-essential work yourself is very difficult to do. It’s impossible to figure out exactly how much frame time remains because after requestAnimationFrame callbacks execute there are style calculations, layout, paint, and other browser internals that need to run. A home-rolled solution can’t account for any of those. In order to be sure that a user isn’t interacting in some way you would also need to attach listeners to every kind of interaction event (scroll, touch, click), even if you don’t need them for functionality, just so that you can be absolutely sure that the user isn’t interacting. The browser, on the other hand, knows exactly how much time is available at the end of the frame, and if the user is interacting, and so through requestIdleCallback we gain an API that allows us to make use of any spare time in the most efficient way possible.

About two thirds of dev.to visitors access via browsers that support requestIdleCallback and the appealing thing about this API is that it is easy to add a simple check for browser support and fall back to default behavior otherwise.

Here is the code for our infinite scroll

  if (('requestIdleCallback' in window) && distanceFromBottom > 1400) {
    requestIdleCallback(function(){ appendPosts(newArticlesHTML) }, { timeout: 1500 });
  } else {
    appendPosts(newArticlesHTML);
  }

What's going on here?

('requestIdleCallback' in window)

This asks whether the API is available.

distanceFromBottom > 1400

This asks if we are still far enough from the bottom to not wait. If we get close, we'll skip the requestIdleCallback portion. So if a user scrolls really quickly, they get posts appended right away.

requestIdleCallback(function(){ appendPosts(newArticlesHTML) }, { timeout: 1500 })

This asks the browser to perform the requestIdleCallback when possible, but not to wait longer than 1.5 seconds.

else {
    appendPosts(newArticlesHTML);
  }

If above conditions are not met, we do not bother with requestIdleCallback. In this case, just calling the function is a good enough scenario. In other circumstances, we could come up with custom logic to bootstrap a similar outcome to requestIdleCallback. Depending on your needs, there are some polyfills available, but your mileage is bound to vary with this sort of API polyfill. I wouldn't blindly rely on any of these.

Final thoughts

This literally just went in, so we might modify the implementation, or realize we need to take a different approach. A big reason I picked this API, though, is because it straightforward to implement and falls back gracefully. I did not have to perform a major cost-benefit analysis because the cost of implementation was so low. If we need to take a different approach, we have not sold ourselves down the river with an overcomplicated API.

Edit: In regards to the aforementioned "on the toilet" remark, I present you this tweet 😳

Discussion

pic
Editor guide
Collapse
raphaeleidus profile image
Raphael Eidus

slight mistake requestIdleCallback(appendPosts(newArticlesHTML), { timeout: 1500 }); needs to be changed to requestIdleCallback(function(){ appendPosts(newArticlesHTML); }, { timeout: 1500 }); otherwise both sides of the if and else execute in the exact same way.

Collapse
ben profile image
Ben Halpern Author

Okay! thanks for the heads up. Interestingly I basically did that in the actual implementation, but I thought I was prettying things up before posting this here. I sometimes forget how JavaScript works in this way until I hit the error and fix. 🙃

I'll update the code in the piece.

Collapse
jvanbruegge profile image
Jan van Brügge

How did you calculate distanceFromBottom?

Collapse
andybons profile image
Andrew Bonventre

I’m very curious how you implemented a seamless back-button experience to prevent pogo-sticking. Care to share your tricks?