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 throughrequestIdleCallback
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 😳
@ThePracticalDev @bendhalpern btw, currently at the toilet scrolling down dev.to 🤔 all about timing16:53 PM - 13 Apr 2017
Top comments (4)
slight mistake
requestIdleCallback(appendPosts(newArticlesHTML), { timeout: 1500 });
needs to be changed torequestIdleCallback(function(){ appendPosts(newArticlesHTML); }, { timeout: 1500 });
otherwise both sides of the if and else execute in the exact same way.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.
How did you calculate
distanceFromBottom
?I’m very curious how you implemented a seamless back-button experience to prevent pogo-sticking. Care to share your tricks?