When I worked on adding a “scroll to top” button to this blog today, I was surprised to find out about performance issues with CSS animations. I always considered CSS to be lighweight and fast, unlike JavaScript (or more accurately, jQuery), but apparently not everything is as simple as it looks.

I was playing around with the following CSS code:

#scrollToTop {
    bottom: -2.5em;
    animation: slideFromBottom 0.8s ease both;

@keyframes slideFromBottom {
    100% { bottom: 0.9em; }

That went along with this JavaScript:

window.onscroll = function() { scrollFunction() }

function scrollFunction() {
    if (window.scrollY > window.innerHeight) {
        document.getElementById("scrollToTop").style.display = "block";
    } else {
        document.getElementById("scrollToTop").style.display = "none";

Nothing fancy there, really. The button #scrollToTop is hidden from sight when loaded, and when the distance from top is greater than the screen size (i.e. one screen has been scrolled) it pops out by changing distance from bottom with a nice animation.
It worked, of course. No surprise there. But then I picked up my smartphone to check how it looked there, and here’s how it was performing on Chrome:
Performance on Chrome (Mobile)

In comparison, this is how it performs on Firefox on my desktop:
Performance on Firefox (Desktop)

Looking into it, I learned about will-change CSS property. It’s supposed to tell the browser which elements are about to be changed, to let it prepare resources for processing them. I tried using will-change: animation, will-change: bottom and even will-change: all, but performance didn’t improve.
Reading about will-change I noticed that it’s usually used with the transform property, so I decided to try and give it a shot, and… voila!

Apparenly, transform properties are more tuned for GPU acceleration, which is widely used by mobile browsers. The more you know…

Oh, and those curious to see how it ended up, here it is:

#scrollToTop {
    background-color: $background-color;
    position: fixed;
    bottom: 0.9em;
    right: 0.9em;
    border: none;
    outline: none;
    cursor: pointer;
    padding: 0.1em 0.7em;
    border-radius: 50%;
    box-shadow: inset -1px -2px #1b1b1b;
    z-index: 999;
    will-change: transform;
    transform: translateY(70px);
    transition: transform 0s ease;
var isDesktop, scrollElement, distanceFromTop
window.onload = function() { scrollFunction() }
window.onresize = function() { scrollFunction() }

function setVars() {
    isDesktop = (window.innerWidth > 992 ? true : false)
    scrollElement = (isDesktop ? document.getElementById("posts") : window)
    distanceFromTop = (isDesktop ? scrollElement.scrollTop : scrollElement.scrollY)
    scrollElement.onscroll = function() { scrollFunction() }

function scrollFunction() {
    var distance = 70 - (distanceFromTop - document.documentElement.clientHeight)
    distance = (distance > 0 ? distance : 0)
    document.getElementById("scrollToTop").style.transform = "translateY(" + distance + "px)";

By the way, I tweaked the appearance logic a little bit to follow the page scroll.
Also, the responsive nature of this blog’s design forced me to bind the scroll event to different elements, based on screen width (that’s what setVars() is for).