Blog (my web musings)
Find out what's interesting me, get tips, advice, and code gems that don't fit elsewhere.
Search Blog
CSS Animations - Performance gone bad (and how to fix it)
- Details
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 lot 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.
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 and box-shadow
All 3 properties fall into the 'styles that affect paint' category.
Improving performance of the snow
I tackled the snowfall first. The demo contains 1 element with 3 background-images, each having different background-position animations applied across the whole screen.
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 }
}
Converting background-position to transform was the obvious choice for the animation, but I wouldn't be able to animate multiple background-images on the same element with a single transform animation. To get around this I created 3 separate animations (for each size snowflake) and applied each snowflake as a background-image to 3 separate elements. I used a :before and :after pseudo element on the <html> element, and an :after pseudo element on the <body>.
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) }
}
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 (inside their containers) while the elements that contain them are animated.
Improving performance of the fairy lights
Next came the fairy lights. What happens in the demo is the opacity of both the background and box-shadow are being faded by changing the alpha-transparency of the layer;
@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) }
}
Unfortunately, animating background and box-shadow is more costly than animating the opacity 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 the opacity. But I don't need the ropes and bases so that's a necessary compromise for me. I therefore converted the animation to a simple opacity fade;
@keyframes flash {
0%, 100% { opacity:1 }
50% { opacity:0.2 }
}
You'll note however, that the original background fades to 0.4 while the box-shadow fades to 0.2, which isn't possible in the conversion because we can only animate the overall opacity 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's box-shadow;
.lights.lights2 li:nth-child(n) { box-shadow:none }
This allows the combined layer of the 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;
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!
Search Blog
Recent Posts
Popular Posts
Latest Scripts
- Scroll Down Before/ After Effect Image Switcher
- Pop-up Text Message with Entrance Effects
- Log and block email spam IPs w/ PHP + .htaccess
- Responsive CSS3 Blinds Effect Slideshow
- AJAX & PHP 5-Star Rating with Flat File Storage
- Defer YouTube Load until Scrolled-To (Lazy-Load)
- Keyboard Accessible 'Tab-to' Menu (JS)
- Defer Image Load until Scrolled-To (Lazy-Loading)
- Scroll Wide Tables w/ Gradient + Indicator (JS)
- Convert anchors to spans (LinkAdv/ Atto in Moodle)