We have more and more animations on the web, and if not careful we can waste a lot of CPU on the client’s browser — causing bad UX (fans spinning up on desktop, battery getting hot, animations not being smooth, etc). As Spider-Man (update: as Joe rightfully mentioned, this quote is actually from Peter's uncle, Uncle Ben) once said: "With Great Power Comes Great Responsibility”.
In the Word Game project (https://github.com/bbc/vjamericas-5-word-game), there was a small animation, with one circle pulsing. The first test code I got from a Codepen was animating a box-shadow. I recreated an example in Codepen:
It looks like this:
It 5 circles pulsing.
If we check the CPU/GPU usage in Chrome: Window -> Task Manager, we can see that the CPU/GPU usage is quite high:
Almost 25% CPU for a simple animation seems quite high.
We can check what’s being repainted on the page, causing this. In Chrome, in the DevTools, we go to the 3 vertical dots -> More tools -> Rendering:
We then check the checkbox Paint flashing:
We now see all the animations constantly flashing green, no bueno:
And if we check the frame rate (show FPS meter), we can see — as expected — that it’s repainting the animation at 60FPS. So 60 times per second, it’s redrawing the shadow, and passing the resulted shadow to the GPU for output (that’s why the GPU usage is quite high as well).
This is not as bad as it could be tho, as it’s only repainting the small area around the circle. I’ve added the property “will-change: transform;” to the element being animated, so that the browser knows this element will change and should be placed on its own layer. Without “will-change: transform;”, it could repaint a much larger area of the screen, increasing the CPU/GPU load even more.
The solution to avoid repaints, is to animate properties which can be repainted directly by the GPU, without the need for the CPU. This is mainly the transform property and the opacity, for example transform: scale, transform: translate, transform: translate3d, opacity, etc. The GPU keeps in memory all the layers that are displayed on screen, and it’s very easy for the GPU to do math stuff, and change the opacity of a layer. For example if you animate a colour, it will be much trickier for the GPU to do. Instead it would be better to have two separate layers and change the opacity of one layer to change the colour.
For the pulsing circles, if we animate the scale instead (transform: scale(1.8);) of the box-shadow, we get exactly the same result:
This time, if we check the repaints, there is none, it just paints it once when the page loads. And if we check the CPU/GPU usage:
We’re now using 6.5 percent CPU instead of 24.4 percent, and 4.1 percent GPU instead of 21.5 percent.
All good 👏😊