Performing a smooth page scroll to an anchor, an invalid input field, or doing more complicated scroll transitions on the web can be quite challenging.
Most websites end up doing expensive DOM operations and manually perform scroll animations.
Browsers have started adding support for smooth behavior on window scroll functions but what happens when there is no reliable way to know the final scroll position in advance (for example when using scrollIntoViewIfNeeded) or when smooth behavior is not supported (e.g. scrollIntoView in Chrome)?
What if there was a way to perform the animation entirely on the GPU and do it without ahead of time knowledge of the final scroll position?
This is an experiment that applies the FLIP technique on an entire document in order to implement smooth page scrolling that works in all modern browsers.
What is FLIP?
FLIP stands for First, Last, Invert, Play. It is essentially a principle, not a framework or a library. It is a way of thinking about animations, and attempting to keep them as cheap as possible for the browser.
Paul Lewis explains it better:
Smooth Scrolling with FLIP
The FLIP principle applied to scrolling boils down to doing a reverse transform with exactly the same amount of pixels that the page scrolled and then letting the browser handle the transition back to zero.
For example let’s say the user is on the top of the page and the target element is 700px below the current scroll position. To perform a smooth scroll a script must:
- Store the current scroll position (pageYOffset is 0)
- Perform the scroll to the target element (pageYOffset is now 700px)
- Translate the document body by 700px using a CSS transform
- Transition the translation back to 0px. This can be done via CSS.
Implementing our demo
Let’s get the CSS code out of the way. There needs to be a simple class that can be attached to the body in order for the browser to animate the translation back to zero.
The animation fill-mode needs to be set to forwards because the document must retain the computed values set by the last keyframe encountered during execution.
That means the document body must end up with no translation, exactly as specified in our keyframes definition. The starting transform will be whatever reverse translation is applied to the document.
And this is a simple script that implements the FLIP technique:
First, it stores the current scroll position and then performs the scroll to the target element.
It then computes the scroll offset in pixels and applies a translation with the same amount of pixels to the body.
Finally, it applies the CSS class which makes the browser run the animation that resets the translation back to zero.
Of course, in a real app some additional things would be required. For example, each time a new scroll is initiated the class name on the document must be removed and the transform reset (or use the onAnimationEnd event to do the cleanup).
There is also an opportunity for early out when scrollIntoViewIfNeeded() decides it does not need to scroll.
This technique really shines with scrollIntoViewIfNeeded() which only scrolls the page when the target element is outside the viewport and it also gives the option to center the target element on the visible area. Both of them would require prohibitive number of calculations and DOM accesses to implement traditionally. With FLIP, we don’t care where & how much the browser decides to scroll.
On touch-enabled devices, focusing input fields immediately after scrolling will launch the on screen keyboard which will cause an additional scroll, messing up FLIP offsets. Adding a delay before focusing solves this problem.
Profiling our proof of concept
OK, it looks like it works. But is it as efficient as we hoped? We need to verify that the scroll animation uses primarily the GPU and hits our FPS target.
First of all, let’s set a baseline by profiling real websites that use traditional scrolling techniques.
Since developers usually have powerful computers, to get better readings in the profile let’s enable a 10x CPU slowdown on Chrome’s Dev Tools. This will also give us a measure of performance in less powerful devices (e.g. smartphones).
So, a 10x CPU slowdown produces the following recording. As expected, frame times exceed the 16.6ms budget, which is the maximum in order to hit 60 frames per second. Some frames even hit the 45ms mark. Despite using requestAnimationFrame the browser cannot keep up:
The profile summary shows zero idle time. Blocking calls to window.scroll dominate the flame graph.
Now let’s profile the demo with the same 10x slowdown. The page contains big photos, many nested elements and unrelated CSS animations running at all times in order to simulate a “heavy” page.
The results are very promising. There are more than 200ms of idle time and it is hitting the frame budget.
There is some slagginess in the beginning of the profile but that is completely normal since JS is still doing some work computing offsets and accessing the DOM. As long as it happens during the first 100ms of the transition then this is what FLIP is all about. Pay a cost upfront but then enjoy a smooth ride.
“There is a window of 100ms after someone interacts with your site where you’re able to do work without them noticing.”
Zooming in our profile, reveals that in the last 500+ms of our animation the CPU is practically idle. Everything has been offloaded to the GPU.