In November 2013 one of our team wrote a great article on SVGs “An introduction to SVG animation”. Since then a lot has changed, most noticeably is Google’s “Intent to deprecate: SMIL”.
But SMIL isn’t dead yet, and I think it is a good time to discuss its limitations, particularly around where and when it should be used. We recently created a web animation for the header of our most recent launch — Postbox — and we learned a lot from the process.
As a company, we have become used to building SVGs with SMIL. For this reason, I didn’t hesitate in passing the build of the animation to one of our junior developers. The result was beautiful; file icons, clouds, sun, birds and boats all moving on screen. Below is a picture:
Unfortunately, it was clear there was problem from the first time I opened it on my Macbook, instead of the high end iMac on which it had been developed. The CPU usage was hitting 100%.
Tip: Chrome has its own usage window (Window -> Task Manager)
At this stage I saw three options:
- Investigate SMIL bottlenecks
- Rebuild with CSS3 animations
- Use a JavaScript library
I decided rebuilding the animation would be the best option, with the added benefit of supporting more browsers. The IE support for the three animation types:
- SMIL: Not supported on any IE
- CSS3: Not supported with objects inside SVGs but IE10+ if used with other elements
- JavaScript: This varies, but IE9+ is a good goal
My search for which option to use for the rebuild lead me to the Google Santa Tracker. The 2014 edition is now built with the Web Animations API which isn’t so well supported.
If you’re curious about it, the source for the Santa Tracker is available here: https://github.com/google/santa-tracker-web
I was more interested in the previous version of the tracker, which had better browser support. Some research revealed that a JavaScript library called GreenSock (GSAP) had been used. The aim of rebuilding the animation in GSAP was to fix the performance problems — and it worked, to a degree. Chrome was now showing around 70-80% CPU usage. I decided it was worth doing some browser testing which revealed something interesting; Firefox was reporting only 50% CPU. This is still high, but a big difference compared to Chrome.
What was Firefox doing to make things better? At first I couldn’t pinpoint what it was, but after a few tests it looked like Firefox does some form of hardware acceleration on SVGs, whereas Chrome does not. So going ahead with this assumption I took the icons in the SVG and converted them PNGs, but keeping them embedded:
<svg>
...
<image xlink:href="/path/file.png" />
...
</svg>
I tweaked the GSAP set up a little and ran the tests again but with no noticeable difference in performance, so I decided to move them outside of the SVG.
<img class="header-icons" src="/header-svg/icons/background.png"/>
<img class="header-icons" src="/header-svg/icons/middleground.png"/>
<img class="header-icons" src="/header-svg/icons/foreground.png"/>
<object class="header-svg" data="/header-svg/whole.svg" type="image/svg+xml"></object>
As the icons were the main focus of the animation we disabled all other animations to review performance. Chrome was showing 20% CPU — this was great. But at this stage I think it’s important to discuss the disadvantages of the PNGs:
- No depth.
- Larger file size
- Not fully scalable
Removing the icons from SVG meant they lost all perception of depth, they used to fall in front or behind buildings before landing in sea. As PNGs they hit the bottom of the image and not the sea.
Losing the building effect was a small sacrifice for such a performance improvement, but we needed to get the icons falling in to the sea again.
That meant splitting the SVG into two pieces; the sky and sea. We left both with the old frame size so we could include both and align to the top.
This was such a simple fix to get the files landing in the sea again. At this stage we were still including the sky SVG as an object meaning it was parseable, this was waste of resources so we changed it to an tag instead.
<img class="header-svg__icons" src="/header-svg/icons/background.png"/>
<img class="header-svg__icons" src="/header-svg/icons/middleground.png"/>
<img class="header-svg__icons" src="/header-svg/icons/foreground.png"/>
<img class="header-svg__sky" src="/header-svg/sky.svg" />
<object class="header-svg__sea" data="/header-svg/ocean.svg" type="image/svg+xml"></object>
Next we focused on adding in the other animations, which would have been very complex to add as PNGs. So we were stuck with non-hardware acceleration animated SVGs.
Adding the boat increased CPU usage up 10%, and waves in the sea another 15%. After testing this across multiple devices, it struggled a little on a 3rd generation iPad, so we removed the sea to find it work nicely across all devices without taking away from the theme of animation.
You can check out the final animation on the postbox site.
Try it yourself
I thought it would be fun to give an example of both Greensock and Web Animations API in action. In this small tutorial we’ll animate a battery like it’s charging:
Or view the Web Animations API (Chrome 43+ only) version here.
Greensock
Getting started with GreenSock couldn’t be easier. Visit their site: http://greensock.com/gsap for their documentation.
In your site, simply include their script:
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/latest/TweenMax.min.js"></script>
Next, you need to decide how you’re going to include your SVG. You could embed it directly in your HTML, likely using a PHP or Twig include :
{% path/file %}
This allows you to access an object in your SVG straight away with jQuery:
$('.battery .charge .top');
But this isn’t always the best solution with large SVGs as they wont be cached, so you can utilise the <object> tag (or <embed>):
<object id="battery" data="/assets/img/battery.svg" type="image/svg+xml"></object>
With this method, you’ll need to grab the SVG first:
var battery = document.getElementById('battery').contentDocument.firstElementChild;
Then you can access objects like so:
$('.charge .top', battery);
From here we can create a timeline and animate the battery:
var battery = document.getElementById('battery').contentDocument.firstElementChild;
var chargeTop = $('.charge .top', battery);
var chargeBottom = $('.charge .bottom', battery);
var green = '#8DC63F';
var yellow = '#F9ED32';
var red = '#F15A29';
var batteryTl = new TimelineMax({ repeat: -1 });
batteryTl.to(chargeTop, 0, { fill: red })
.to(chargeTop, 3, { attr: {'width': '+=240'} } )
.to(chargeBottom, 3, { attr: {'width':'+=240'} }, '-=3')
.to(chargeTop, 0.5, { fill: yellow }, 0.5)
.to(chargeTop, 0.5, { fill: green }, 1.5)
For more information you can check out the docs or have a look at the following codepen examples.
Web Animations API
The web animations api has limited support so I recommend viewing in Chrome 43+ only. You can find more information at: W3C Spec.
Just like before we’ll be using an object:
<object id="battery" data="/assets/img/battery.svg" type="image/svg+xml"></object>
And accessing it like so:
var battery = document.getElementById('battery').contentDocument.firstElementChild;
Then to access objects, you use:
var chargeTop = battery.querySelector('.charge .top');
From here we can add our two players, one for the top charge and one for the bottom:
var battery = document.getElementById('battery').contentDocument.firstElementChild;
var chargeTop = battery.querySelector('.charge .top');
var chargeBottom = battery.querySelector('.charge .bottom');
var green = '#8DC63F';
var yellow = '#F9ED32';
var red = '#F15A29';
var player = chargeTop.animate([
{
width: '0',
fill: red,
offset: 0
},
{
fill: yellow,
offset: 3/4
},
{
width: '240px',
fill: green,
offset: 1
}
], {
duration: 3000,
iterations: Infinity,
easing: 'ease-in'
});
var player2 = chargeBottom.animate([
{
width: '0',
offset: 0
},
{
width: '240px',
offset: 1
}
], {
duration: 3000,
iterations: Infinity,
easing: 'ease-in'
});
If you want to extend the support of WAAPI to more browsers. You can include the polyfill from here: https://github.com/web-animations/web-animations-js.
Let’s include the web animations script, download it from the repo and add it like so:
<script src="/assets/js/web-animations.min.js"></script>
Unfortunately it does’t work with the <object> tag (or <embed>) so you will need to include your SVG directly. And the second caveat, browsers like Firefox and Safari can’t animate the attribute width so the result is little hacky using transforms. Check the Polyfill example here.
Summary
I’m not recommending that people stop using SMIL; in fact, it can be useful when animating a small icon, with the benefit of not including any extra scripts. When SMIL isn’t supported, it will appear static, which is a sensible fallback.
If you’re tackling larger animations like the Postbox example, GSAP will save you a lot of time. And if you hit any performance bottlenecks, moving from an object inside SVG to PNG requires minimal changes.
This next year will be interesting to see browser support extended for the Web Animations API – so have fun animating 🙂