CSS Animations - when performance goes bad (and how to fix it)
by
, 12-16-2016 at 09:58 AM (23403 Views)
Through the power of CSS3, animation of most HTML elements can nowadays be done without using JavaScript or Flash. Gone too (almost) are the days when images would be blinked on and off with an animated GIF. There are still times, however, when a kitschy garnish can be just the thing to add a bit of festive fun to a normally tasteful web page. There's a time and a place for everything and if we can't have fun at Christmas, well, when can we!?
But let's not go overboard. Everything we add to a web page should be considered for (amongst other things) performance impacts. CSS3 animations can, unfortunately, slap performance in the face when they don't follow the rules of the web. Cutting to the chase, there are expensive animations (ones that trigger layout and paint changes) and there are cost-effective animations (ones that don't trigger layout and paint changes).
Animating layout properties
Layout happens when a change to an element affects the position and size of it and other elements. For example, if a div's height changes, other elements may move around to fill the gap that the reduced-height div left. Depending on the complexity of the web page, that's a lots of recalculations (of new sizes and positions) for the web browser to perform at every frame of animation.
Animating paint properties
Changing an element may also trigger (re)painting at each new animation frame state, and not for just the element that has changed, but for other layers in the group too.
The rules of performance-friendly animations
To get the most out of your CSS animations, you should stick to properties that don't affect or depend on the document flow, and that don't cause a repaint. Transforms are the most cost-effective properties to animate in CSS because they bypass the style recalculations, layout and paint stages, and go straight to the final stage of compositing (drawing) the layers to the screen. The properties we should therefore aim to animate are;
- opacity
- rotate
- scale
- translate
Opacity is a strange one because it doesn't cause repaints as you might expect. It can instead be offloaded to the GPU. In simple terms, the element is turned into an image layer during the transition, and composited to the screen, thus avoiding any style recalculations, layout, changes or paints, that would normally happen beforehand.
Breaking the rules = costly animations
The problem with costly animations is that they thrash the CPU, which to the average web user means flickering screens, whirring fans, unresponsive web pages and browser crashes. I experienced this a few days ago when I was asked to deck digital signage screens with something festive for Christmas. Naturally, I headed off to Google to look for inspiration; a CSS3 Snow Animation demo and a Pure CSS Christmas Lights demo came up as likely candidates, and I dutifully inserted them into the existing web page to see the effect.
DEMO 1 (costly animations): http://fofwebdesign.co.uk/template/_...ignage-old.htm
View the demo above and listen for a few seconds as your computer prepares for flight... at least that's how it sounds with the fans kicking in to overdrive. What's more, depending on computer spec, there's an annoying flicker every couple of seconds. The more simplistic demos do not exhibit the same behaviour, but there's a lot more going on in the digital signage screen, with fading slideshow and scrollers, etc. There's a lot more for the CPU to contend with.
Thinking back to the rules of performance-friendly animations, I quickly scrambled for my bookmarks to remind myself which properties are most costly, while inspecting the animated properties behind the newly inserted Chrimbo treats;
- The CSS3 Snow Animation animates 3 instances of
background-position
- And the Pure CSS Christmas Lights animates
background
andbox-shadow
All 3 properties fall into the 'styles that affect paint' category.
Improving performance - Snow
I tackled the snowfall first. The demo contains 1 element with 3background-images
, each having differentbackground-position
animations applied across the whole screen.ConvertingCode:html:after { content:''; position:absolute; z-index:-1; top:0; left:0; bottom:0; right:0; background-image:url(images/snow1.png), url(images/snow2.png), url(images/snow3.png); -webkit-animation:snow 10s linear infinite; animation:snow 10s linear infinite; } @keyframes snow { 0% { background-position:0 0, 0 0, 0 0 } 50% { background-position:500px 500px, 100px 200px, -100px 150px } 100% { background-position:500px 1000px, 0 400px, 0 300px } }background-position
totransform
was the obvious choice for the animation, but I wouldn't be able to animate multiple background-images on the same element with a singletransform
animation. To get around this I created 3 separate animations (for each size snowflake) and applied each snowflake as abackground-images
to 3 separate elements. I used a:before
and:after
pseudo element on the <html> element, and an:after
pseudo element on the <body>.The size and position of the elements are interesting here because, in the demo, the element that holds the snowflakes is the same size as the screen (anchored to all 4 corners) and the snowflake positions animate individually inside it. With the new and improved version (a demo for that is at the bottom of this page), the elements that hold the snowflakes are much larger; positioned absolutely off-screen with negative offsets that correlate to the maximum range of movement defined in the transforms. Here, the snowflakes remain at fixed coordinates (relative to their containers) while the elements that contain them are animated.Code:html:before { content:''; position:absolute; z-index:0; left:-500px; top:-1000px; right:0; bottom:0; background:url(images/snow1.png); -webkit-animation:snow-1 10s linear infinite; animation:snow-1 10s linear infinite; } html:after { content:''; position:absolute; z-index:-1; left:-100px; top:-400px; right:0; bottom:0; background:url(images/snow2.png); -webkit-animation:snow-2 10s linear infinite; animation:snow-2 10s linear infinite; } body:after { content:''; position:absolute; z-index:-1; left:0; top:-300px; right:-100px; bottom:0; background:url(images/snow3.png); -webkit-animation:snow-3 10s linear infinite; animation:snow-3 10s linear infinite; } @keyframes snow-1 { 0% { transform:translate3d(0,0,0) } 50% { transform:translate3d(500px,500px,0) } 100% { transform:translate3d(500px,1000px,0) } } @keyframes snow-2 { 0% { transform:translate3d(0,0,0) } 50% { transform:translate3d(100px,200px,0) } 100% { transform:translate3d(0,400px,0) } } @keyframes snow-3 { 0% { transform:translate3d(0,0,0) } 50% { transform:translate3d(-100px,150px,0) } 100% { transform:translate3d(0,300px,0) } }
Improving performance - Fairy Lights
Next came the fairy lights. What happens in the demo is the opacity of both thebackground
andbox-shadow
are being faded by changing the alpha-transparency of the layer;Unfortunately, animatingCode:@keyframes flash-1 { 0%, 100% { background:#ff0; box-shadow:0 5px 24px 3px #df0 } 50% { background:rgba(255,255,0,0.4); box-shadow:0 5px 24px 3px rgba(0,247,165,0.2) } } @keyframes flash-2 { 0%, 100% { background:cyan; box-shadow:0 5px 24px 3px cyan } 50% { background:rgba(0,255,255,0.4); box-shadow:0 5px 24px 3px rgba(0,255,255,0.2) } } @keyframes flash-3 { 0%, 100% { background:#f70094; box-shadow:0 5px 24px 3px #f70094 } 50% { background:rgba(247,0,148,0.4); box-shadow:0 5px 24px 3px rgba(247,0,148,0.2) } }background
andbox-shadow
is more costly than animating theopacity
property. I know why the original developer did it this way though - because of the joining ropes and bulb bases, which would also fade if we were to animate theopacity
. But I don't need the ropes and bases so that's a necessary compromise for me. I therefore converted the animation to a simpleopacity
fade;You'll note however, that the originalCode:@keyframes flash { 0%, 100% { opacity:1 } 50% { opacity:0.2 } }background
fades to 0.4 while thebox-shadow
fades to 0.2, which isn't possible in the conversion because we can only animate the overallopacity
of the entire element. How did I overcome this? With a dirty trick; I duplicated the fairy lights HTML in the markup, but removed the dupe'sbox-shadow
;This allows the combined layer of theCode:.lights.lights2 li:nth-child(n) { box-shadow:none }background
(from 2 strings of overlapping fairy lights) to fade down to 0.4.
Putting all of our performance-friendly animations together, we now get this;
Here's the result - DEMO 2 (less-costly animations): http://fofwebdesign.co.uk/template/_...ds/signage.htm
Notice how in the revised demo above your computer fans aren't whirring so heavily now? The flicker has also gone too.
That's a real-world example of making allowances and putting in extra effort for the sake of performance. Please follow my lead and plan carefully if you ever find yourself adding potentially costly animations to a web page.
Merry Christmas!