As we know, it's often advised to debounce scroll listeners so that UX is better when the user is scrolling.
However, I've often found libraries and articles where influential people like Paul Lewis recommend using requestAnimationFrame
. However as the web platform progress rapidly, it might be possible that some advice get deprecated over time.
The problem I see is there are very different use-cases for handling scroll events, like building a parallax website, or handling infinite scrolling and pagination.
I see 3 major tools that can make a difference in term of UX:
So, I'd like to know, per usecase (I only have 2 but you can come up with other ones), what kind of tool should I use right now to have a very good scroll experience?
To be more precise, my main question would be more related to infinite scrolling views and pagination (which generally do not have to trigger visual animations, but we want a good scrolling experience), is it better to replace requestAnimationFrame
with a combo of requestIdleCallback
+ passive scroll event handler ? I'm also wondering when it makes sense to use requestIdleCallback
for calling an API or handling the API response to let the scroll perform better, or is it something that the browser may already handle for us?
Although this question is a little bit older, I want to answer it because I often see scripts, where a lot of these techniques are misused.
In general all your asked tools (
rAF
,rIC
and passive listeners) are great tools and won't vanish soon. But you have to know why to use them.Before I start: In case you generate scroll synced/scroll linked effects like parallax effects/sticky elements, throttling using
rIC
,setTimeout
doesn't make sense because you want to react immediately.requestAnimationFrame
rAF
gives you the point inside the frame life cycle right before the browser wants to calculate the new style and layout of the document. This is why it is perfect to use for animations. First it won't be called more often or less often than the browser calculates layout (right frequency). Second it is called right before the browser does calculate the layout (right timing). In fact usingrAF
for any layout changes (DOM or CSSOM changes) makes a lot of sense.rAF
is synced with the display refresh rate (vsync) as any other layout rendering related stuff in the browser.using
rAF
for throttle/debounceThe default example of Paul Lewis looks like this:
This pattern is very often used/copied, although it makes little till no sense in practice. (And I'm asking myself why no developer sees this obvious problem.) In general, theoretically it makes a lot of sense to throttle everything to at least the
rAF
, because it doesn't make sense to request layout changes from the browser more often than the browser renders the layout.However the
scroll
event is triggered every time the browser renders a scroll position change. This means ascroll
event is synchronized with the rendering of the page. Literally the same thing thatrAF
is giving you. This means it doesn't make any sense to throttle something by something, that is already throttled by the exact same thing per definition.In practice you can check what I just said by adding a
console.log
and check how often this pattern "prevents multiple rAF callbacks" (answer is none, otherwise it would be a browser bug).As you will see this code is never executed, it is simply dead code.
But there is a very similar pattern that make sense for a different reason. It looks like this:
With this pattern you can successfully reduce or even remove layout thrashing. The idea is simple: inside of your scroll listener you read layout and decide wether you need to modify the DOM and then you call the function that modifies the DOM using rAF. Why is this helpful? The
rAF
makes sure that you move your layout invalidation (at the end of the frame). This means any other code that is called inside the same frame works on a valid layout and can operate with super fast layout read methods.This pattern is in fact so great, that I would suggest the following helper method (written in ES5):
requestIdleCallback
Is from the API similar to
rAF
but gives something totally different. It gives you some idle periods inside of a frame. (Normally the point after the browser has calculated layout and done paint, but there is still some time left until the v-sync happens.) Even if the page is laggy from the users view, there might be some frames, where the browser is idling. AlthoughrIC
can give you max. 50ms. Most of the time you only have between 0.5 and 10ms to fulfill your task. Due to the fact at which point in the frame life cyclerIC
callbacks are called you should not alter the DOM (userAF
for this).At the end it makes a lot of sense to throttle the
scroll
listener for lazyloading, infinite scrolling and such usingrIC
. For these kinds of user interfaces you can even throttle more and add asetTimeout
in front of it. (so you do 100ms wait and then arIC
)Here is also an article about
rAF
, that includes two diagrams which might help to understand the different points inside of a "frame lifecycle".Passive event listener
Passive event listeners were invented to improve scroll performance. Modern browsers moved page scrolling (scroll rendering) from the main thread to the composition thread. (see https://hacks.mozilla.org/2016/02/smoother-scrolling-in-firefox-46-with-apz/)
But there are events that produce scrolling, which can be prevented by script (which happens in the main thread and therefore can revert the performance improvement).
Which means as soon as one of these events listeners are bound, the browser has to wait for these listener to be executed before the browser can compute the scroll. These events are mainly
touchstart
,touchmove
,touchend
,wheel
and in theory to some degreekeypress
andkeydown
. Thescroll
event itself is not one of these events. Thescroll
event has no default action, that can be prevented by script.This means if you don't use
preventDefault
in yourtouchstart
,touchmove
,touchend
and/orwheel
, always use passive event listeners and you should be fine.In case you use preventDefault, check wether you can substitute it with the CSS
touch-action
property or lower it at least in your DOM tree (for example no event delegation for these events). In case ofwheel
listeners you might be able to bind/unbind them onmouseenter
/mouseleave
.In case of any other event: It does not make sense to use passive event listeners to improve performance. Most important to note: The
scroll
event can't be canceled and therefore it never makes sense to use passive event listeners forscroll
.In case of an infinite scrolling view you don't need
touchmove
, you only needscroll
, so passive event listeners do not even apply.Resume
To answer your question
setTimeout
+requestIdleCallback
for your event listeners and userAF
for any layout writes (DOM mutations).rAF
for any layout writes (DOM mutations).