1180Days

1180 days since this post was last revised. Specific details are likely out of date.

Research indicates that SVG images served as an image sprite display faster than SVG images as data URIs. But how much faster? And is it worth your time? If it is, how do we actually produce the SVG sprites? This post looks at different data on the sprite versus data URI question and how to configure a couple of automated Grunt tasks to produce SVG image sprites and fall back PNG sprites (rather than data URIs) to provide resolution independent interface icons if you so chose.

Note: this is not a post about ‘responsive images’. I’m talking purely about interface images/icons. The kinds of icons that are common in web-based applications; the sort used to intimate actions and functionality to users.

I generally favour leaving older browsers behind; here’s a snippet that encapsulates that opinion from ‘Responsive web design with HTML5 and CSS3’:

… developing and enhancing the user experience for modern browsers, rather than patching up the holes in old ones makes more sense. At least to me!

So, to embody this stance, for personal projects or instances where I’m merely bashing out a proof of concept, I opt for using an icon-font for simple graphical assets. However, crucially, icon fonts have no support in Windows Phone 7.5 and below (and worse still it reports a false positive when feature testing) or Android 2.1. Now, it’s not that icon fonts are a lost cause. Zach Leatherman has written a comprehensive post about how you can make icon-fonts more accessible but this point illustrates that when it comes to providing HiDPI assets on the web, there is no perfect solution that covers all possibilities.

There are occasions however, when it is imperative to provide an image, even for ailing mobile platforms/browsers. Falling back to something only vaguely similar or reminiscent of the intended icon or merely a text string representing the intended icon isn’t an option (sadly). For example, critical UI elements such as disclosure triangles, favourite buttons, menu, save and load icons that constitute core functionality.

Here’s a list of fairly popular platforms/devices that I think it’s reasonable to need core UI support for:

  • Android 2.1+
  • Windows Phone 7.5+
  • iOS 4+
  • Blackberry 7+
  • Modern Browsers (IE9+)
I encourage you to read Ian Feather’s write up on the choice of SVG they made at Lonely Planet. It covers the pro’s and con’s of choosing SVGs over an icon font far better than I will here.

In terms of the format assets should be delivered, I came to the same conclusion as the Lonely Planet (and plenty of others) but I don’t necessarily agree that using data URIs are the best way to serve them up. But I’m getting ahead of myself.

For prosperity (and in case link rot sets in with Ian’s post), here are the bullet point pro’s and con’s I settled on for both icon fonts and SVGs.

Icon Fonts For:

  • fast iteration: colour, size and borders/shadows can be changed and added easily via CSS using font-size, colour and text-shadow
  • an icon font provides a single, very small asset for all icons
  • it is simple to administer changes to the icon set via online apps such as icomoon.io

Icon Fonts against:

  • Icon fonts are (largely) single colour only
  • Font glyph bounding boxes are difficult to get right (the bounding box for glyphs should only be large as the largest glyph — which is often unknown when making the asset as additional (larger) icons might need adding in future). This often means additional unwanted space around glyphs. This can make positioning tricky in some instances.
  • No support for Windows Phone 7.5 (and no ability to simply fall back as Modernizr @font-face tests report a false positive)
  • No support in Android 2.1
  • No easy way to add a border if an icon font is nested in an element. For example, a left border follows the icon font so when positioning the icon, the border follows it (although it could be nested inside an element and moved independently).
  • Occasional differences in font-rendering across browsers can mean the position of elements differs slightly. For example, the nib on a drop-down menu can appear a few pixels out on one device (e.g. iOS v Android) compared to another.

Binary images (SVG & PNG) for:

  • Multiple colours possible for each asset
  • Fallbacks can be provided for all situations (SVG served first with PNG fallback)
  • Easier to achieve consistent positioning across different UAs as background-position property and rendering is pretty consistent

Binary images (SVG & PNG) against:

  • Fallback PNG images needed for older browsers e.g. Android < 2.3 (although automatic creation of fallback assets is simple)
  • Slower iteration when prototyping – colour changes, borders and shadows have to be done in a graphics app or coding the SVG directly

So, icon-font or binary image?

An icon font means no support for Android 2.1 or Windows 7.5 and no sensible fallback option so, much as I usually enjoy working with an icon font (I’m using them here on this site), for crucial UI for the widest base of users, they just aren’t an option.

The die is cast. SVG with PNG fallback it is

If using SVG/PNG assets we need some sane way of creating, updating and applying them.

Thankfully, some time ago, those super smart people at the Filament Group wrote ‘Grunticon’, something I already used for my own purposes in the past. However, as thorough as the tool is, it produces assets as data URIs embedded into the CSS. In the past I’ve been a big fan of data URIs for small assets but then I read a a series of posts on Mobify which made me reconsider their wholesale applicability for UI elements on mobile.

Here is a key quote:

Regardless of whether the data URI content exists in a cached CSS or HTML file, the browser must decode the image each time a page renders: the decoding cost is paid repeatedly every time a page is viewed.

This part in a related piece perhaps more relevant:

We can see from the results that in the cached condition for both Android 4.2 and iOS 6 that sprites offer roughly 2X performance with a difference of around 220ms for iOS and 70ms for the Android 4.2 native browser. Chrome on Android and Firefox fare a bit better, with about a 50% or ~60 ms performance difference in favor of CSS sprites.

Off the back of this research, rather than take it at face value, I thought it would be worth testing whether the practice of delivering UI assets as data URIs was flawed from a performance point of view compared to the exact same assets served as an image sprite.

So each implementation was tested across a number of devices (most ‘mobile’, some ‘desktop’ class); common devices it made sense to evaluate the performance of each delivery mechanism on.

Test methodology

Two web pages were made. Each with 62 identical divs named with incrementing classes (e.g. ‘cat-1′,’cat-2’). The two pages were identical except for the fact that one linked to a CSS file containing background-position co-ordinates and a link to a binary SVG image sprite while the second linked to a CSS file containing data URIs of all the requisite SVG images. This provided a simple A/B style test to compare the relative merits of the delivery format.

The purpose of the test was to ascertain the speed it takes the User Agent to render the assets to screen. To this ends the Navigation Timing API was used.

Here was the JavaScript snippet to serve this aim that was placed at the bottom of each page:

<script type="text/javascript">
	;(function TimeThisMother() {
		window.onload = function(){
			setTimeout(function(){
			var t = performance.timing;
				alert("Load time is: " + (t.loadEventEnd - t.responseEnd) + " milliseconds");
			}, 0);
		};
	})();
</script>

This snippet alerted the difference between the responseEnd event (when all assets have been received from the server) and the loadEventEnd event (when everything has finished loading/displaying) in milliseconds. Essentially this provides a figure to compare how long it took the browser to render each asset delivery type to the screen.

The tests were performed in a fairly crude manner, manually refreshing each device 10 times on each page and recording the result.

Note: sadly, iOS does not currently support the Navigation Timing API so it was not possible to test it in this manner.

Here are the headline (averaged) results.

SVG sprite data URI
Totals 129.23 224.95
Totals (sans Win Phone 8S) 121.64 131.54

Of the devices tested, only Windows Phone 8S seemed to give a large discrepancy between the two types of delivery. There was a difference of 524 milliseconds there (in that the sprite was 524ms faster). That one device certainly skewed the results.

Interestingly, desktop Chrome was also more than twice as slow at rendering the data URIs. However, even with the Chrome result in there, once the Windows Phone 8S result is removed, for all other devices the difference between using data URIs and an image sprite was just under 10 milliseconds.

Delivery mechanism summary

In this limited ‘real world’ test, the difference between serving assets as an image sprite or as data URIs in the CSS seems negligible. Until iOS can be tested in a similar fashion, and unless it gave wildly differing results in favour of one camp, it doesn’t seem reasonable to insist on one delivery mechanism over another.

The image sprite, across multiple devices was clearly faster but it’s for you to decide whether the difference is worth upsetting any workflow you may already have set up.

A workflow for the ‘gold’ standard

Suppose you are free to choose between making assets as data URIs or an image sprite. You may as well go for the better performing image sprite, right? Probably. I think there is a little more effort required down that route.

Be aware that if you want to allow zooming of your pages, there is one distinct disadvantage to using an image-sprite; image-sprites as background-images don’t scale consistently across browsers..

There are plenty of well known tools, such as Grunticon, to handle creating assets as data URIs but I couldn’t find many to handle creating SVG assets as an image sprite along with a PNG fallback. What follows is one possible workflow.

Grunt

We’ll be using the Grunt JS task runner to handle all this. I know the cool kids are using Gulp but the needed plug-ins are only available with Grunt at present.

Requisite tools

First off, we save our individual SVG files into a folder (using Sketch or Illustrator for example). I’m saving mine into a folder called ‘img/ui-separate’ but it can be whatever, as long as the config matches.

Then we want to firstly optimise the SVGs (using svg-min) and then combine them using Dr Grunt SVG Sprites into an SVG and PNG image sprite with relevant CSS/Sass (incidentally the ‘Dr’ there is the Danish Broadcasting Corporation, the Danish equivalent to the BBC). Let’s look at each task in a little more detail.

As Rasmus Fløe, the Dr Grunt SVG sprites plugin maker explains in the comments below, if you only want to make a sprite with SVGs, there is no need to configure and run svgmin as a separate task as it’s included with Dr Grunt SVG sprites already. I’m splitting the task out here as sometimes I create data URI assets from the SVGs and sometimes sprites.

svgmin

The svg-min task removes unneeded cruft from an SVG. It’s amazing how much rubbish get’s cleared out (unneeded xlmns namespaces, defs, whitespace etc). Typically I find a good 30%+ space saving. Lot’s of people/organisations skip this step so their served SVGs (whether as sprites or data URIs) aren’t as lean as they could/should be. Let me try and convince you it’s essential.

Here’s an example of a simple SVG exported from Sketch:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="48px" height="46px" viewBox="0 0 48 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
    001Star
    Created with Sketch (http://www.bohemiancoding.com/sketch)
    
    
        
    
</svg>

That’s how many people then convert it to data URI or an image sprite. However, by first processing it with svgmin, this is the SVG before it gets converted to a data URI or image sprite:


<svg width="48" height="46" viewBox="0 0 48 46" xmlns="http://www.w3.org/2000/svg">001Star</svg>

Svgmin reported a 64% saving here!

The svgmin Grunt task

So, first of all an svg-min task is needed in the Gruntfile that looks something like this:

/*
    Clean SVGs from Illustrator/Sketch
*/

svgmin: {
    options: {
        plugins: [
            { removeViewBox: false },
            { removeUselessStrokeAndFill: false }
        ]
    },
    ui: {                                         // Target
        files: [{               // Dictionary of files
            expand: true,       // Enable dynamic expansion.
            cwd: 'img/ui-separate',     // Src matches are relative to this path.
            src: ['**/*.svg'],  // Actual pattern(s) to match.
            dest: 'img/ui-separate',       // Destination path prefix.
            ext: '.svg'     // Dest filepaths will have this extension.
            // ie: optimise img/src/branding/logo.svg and store it in img/branding/logo.min.svg
        }]
    }
},

This gets run with the grunt svgmin command (or combined into another task). Once svgmin processes the SVGs, it will provide the leanest possible SVGs to build the sprite assets from.

svgmin task

Now for the automagical creation of the two sprite sheets (one SVG and one PNG) and the associated styles.

The svg-sprites Grunt task

This task will create an SVG and PNG image sprite from the individual SVGs.
When configuring the svg-sprites Grunt task, a different ‘job’ is needed for each image sprite needed. Only one may be needed but the option to configure more is available. For example, perhaps one type of sprite is needed for certain pages and another sprite for others. It doesn’t necessarily make sense to have one enormous image sprite for all assets. Here’s an example of a single job:

"svg-sprites": {
            "ui-separate": {
                options: {
                    spriteElementPath: "wp-content/themes/bf2013/img",
                    spritePath: "wp-content/themes/bf2013/img/ui",
                    cssPath: "sass/_partials/_modules/_ui",
                    cssSuffix: "scss",
                    cssPrefix: "_",
                    prefix: "Ui",
                    sizes: {
                        std: 18
                    },
                    refSize: 17,
                    unit: 20
                }
            }
        }

What’s important here is that the “ui-separate” part matches the folder created above. To see an example with two or more sprites defined take a look at the GitHub pages for Dr Grunt SVG Sprites.

Once these tasks are run, there will be two image sprites:

SVG Sprites task
  • An SVG sprite for supporting browsers
  • A PNG fallback for all the others

Here’s an example of the SVG image sprite:

You can prevent the task producing CSS but it generates it by default (if you re-size the sprite using background-size the generated CSS will be largely useless). Here’s an example of the positioning info it generates:

.Ui-001Star-std {
    width: 51px;
    height: 49px;
    background-position: -0px 0;
}

Serving the image sprite at different sizes

Sometimes it will be necessary to use the image sprite differently. For example, perhaps a shopping basket needs to be shown at one size in one situation and a different size in another.

It’s not possible to be entirely prescriptive about how to go about this. While in some instances the generated CSS will be all that is needed, in others, manual sizing and positioning will be required. This can be laborious but offers very precise positioning of the elements.

I’ll offer my own point of view here and how I use them but your own mileage may vary.

I tend to make a joint selector and placeholder in Sass and use/extend when needed. For example:


.UiImg,
%UiImg {
    background-image: url('#{$img}ui/ui-separate-sprite.svg');
    background-repeat: no-repeat;
    .no-svg & {
        background-image: url('#{$img}ui/ui-separate-std-sprite.png');
    }
}

It’s then possible to use this ‘utility’ class directly on an HTML element. Alternatively, use the placeholder selector to extend the base functionality (background-image and no-repeat properties) that has been set and then add size and position values in addition to the @extend on whatever selector you like.

Fallback with Modernizr

The usefulness of Modernizr should be apparent in the above snippet: it provides the fallback class for when SVG is supported.

Stopping image sprite ‘bleeding’

Now, you may be skipping ahead mentally and wondering how positioning an image sprite is going to work well in complex UI situations. For example, one of the big bonus points for using data URIs is that they contain only that image (without the other images of a sprite also being part of the background-image). When using a sprite, there is some chance of the image sprite ‘bleeding’ across an element.

The solution I use is simply placing the background image on a ::before or ::after pseudo class. This way I am able to specify a limiting box on which to apply the background-image. For example:

.no-bleed {
    position: relative;
    &:after {
        @extend %UiImg;
        position: absolute;
        content: " ";
        width: 1rem;
        height: 1rem;
        display: block;
        top: 0;
        left: 0;
        background-position: -whatever 0;
        background-size: auto 1rem;
    }   
}

Here’s an example using this technique to prevent ‘bleed’; a full width div with a single UI element (the star of the sprite) placed at one end:

Example of placing UI image with pseudo class

It’s a slight trade-off but I haven’t yet come up against a situation where I have felt compromised. It’s also possible you can add more white space around each image or opt for a different orientation (vertical image sprite as opposed to horizontal say)

General implementation notes:

There have been a few ‘gotchas’ while using this workflow. They are documented below (it was cathartic for me to do so and I hope it may help you):

Ordering images and adding images to the sprite over time

  • If adding an image to the sprite, ensure its given a name with the next sequential order. Otherwise the positioning of existing images may be affected (e.g. if the new image gets positioned before the existing ones the background-position property will be off). So for example, name the images in the folder with a prefix like this 001image1.svg, 002image-sausages.svg.

CSS properties and compatibility

  • Ensure that background-color is used for elements that need both an image and a background-color. Specifying only background on an element will conflict.

  • The generated image-sprite is created as a horizontal sprite. Therefore, background-size: auto 20px; – will size it with the unit given as the height the image should appear at. Leave the first part as auto and the sprite will then scale as needed.

  • Values for background-size should be set with px for maximum compatibility as older mobile browsers don’t support background-size: cover;.

Set the position of the image:

  • background-position: -100px 29px – use this to position the sprite. Be aware that you may want to use percentages rather than pixels to cater for screen zooming (use Ethan Marcotte’s good ol’ responsive target/context formula).

The first (usually negative) value pulls the sprite along and the second positions on the y axis.

iOS browser bug

I opened a bug for this oddity here: https://bugs.webkit.org/show_bug.cgi?id=128248

It seems that if SVG sprites are created too ‘large’ in terms of their width and height attributes, then iOS is unable to render them using background-size.

Testing methodology: saved out 16 basic SVGs (a star shape) and converted them into an image sprite. Once as SVGs with a width 28 x 28 and once as an image 960 x 560. The ‘smaller’ sprite renders, the larger one does not.

Don’t use width/height 100%

It’s not possible to set the source SVG to width/height 100% as the Grunt task ‘svg-sprites’ is not then able to process the individual SVGs into a sprite.

SVG rendering is possibly still slower than PNG rendering in browsers

I’m not able to quantify this on anything other than Chrome. However, as a general rule, I believe SVGs will likely render slower than equivalent PNGs. Test this with your own sprite by enabling continuous paint repainting and, assuming Modernizr is being used (or something similar) change the class from svg to no-svg in the html tag and watch the paint time drop.

This isn’t enough to make me want to ditch SVGs as they look incredible compared to even @2X PNG assets but it is still something to be aware of.

Summary

If you need to support platforms like Android 2.1 and Windows Phone 7.5, font-face is likely to leave you wanting. SVG with a PNG fallback gets you better coverage.

If opting for SVG with PNG fallbacks, image sprites are the best performing manner in which to serve them. However, consider the positioning complexity of an image sprite. Succinctly, for an easier implementation, choose data-uri’s. For the best performance, choose an image sprite.

  • a sprite requires the UA to download another asset but it will likely be on a warm connection and is no worse than loading an extra CSS file loaded with data URIs
  • when data URI loading was faster than image sprite loading the difference was minimal, when image sprites were faster than data URIs the difference was sometimes significant
  • It’s far easier and more consistent to position images that are data-uri’s than it is to position elements within an image sprite.
  • doing without an icon-font makes prototype iteration slower but it means that any code that makes it to production is already more robust for the next team to pick up and run with
  • SVGs render slower than PNGs. Not a deal breaker but something to be aware of
  • Using a build tool like Grunt takes much of the manual labour, pain and human error out of creating UI image assets and supporting code

If anyone has ideas/thoughts on how to improve this workflow or issues not covered, I welcome your feedback.