Wednesday, November 21, 2012

Optimizing image sprites for high-density displays with SVG


Author: Jay Ayres


Image spriting is a well-known technique for improving webpage load performance. Performance is greatly improved by reducing the total number of resource requests to the server, whether those resources are CSS files, Javascript files, or image files. Spriting is the process of bundling all image files into a single large image, combining those resource requests into one. CSS is used to specify the proper X and Y offsets in the sprite of each component image.

.sp-arrowBack 
{ 
  background-image: url('/sprites/mobile_pack_1.png'); 
  background-position: left -371px; width: 51px; height: 27px; 
}
.sp-hotels 
{ 
  background-image: url('/sprites/mobile_pack_1.png'); 
  background-position: left -411px; width: 17px; height: 13px; 
}
Fig. 1: Example image sprite and CSS

However, TripAdvisor’s mobile team faced a dilemma when optimizing our site for high-pixel-density, retina displays such as those found on the iPhone 4, the iPad, newer Android devices, and higher-end laptops. Images from our existing icon sprites appeared blurry on the high-resolution displays, and we had to determine an efficient way to maintain page performance while sharpening the images.


High-density icon sprite?


A straightforward approach would have been to create a new icon sprite that had two times the width and height of our previous sprite. In the short-term, this approach would work; Mobile Safari on retina-enabled iOS devices displays a sharp image when the width and height of the source image are 2x the specified width and height in CSS.

A 2x icon sprite presented a few notable drawbacks. First, the size of the sprite image file balloons when it is increased to 2x. Both the width and height are doubled, so the number of pixels increases by a factor of 4. Also, we would have had to create multiple variants of the high-density icon sprite to optimize for all high-resolution displays. High-pixel-density displays vary in the ratio between the logical width of the viewport and the number of pixels for that logical viewport width. While the iPhone and iPad are consistent in the 2x scale-up factor, some Android devices scale to different densities, and in the future we may see even higher-resolution displays that would necessitate creation of a 4x icon sprite. Up-scaling bitmap images is a time-consuming process that requires re-creation of the image from scratch, unless a vector-based representation of the image is saved somewhere.

TripAdvisor instead decided to convert all images on our mobile and tablet sites over to scalable vector graphics (SVG). While this process was initially time-consuming, it will pay dividends as we no longer need to worry about optimizing for different screen pixel densities by adding new icon sprites.

SVG has quickly been gaining support in the most common web browsers. SVG images, specified as background images in CSS, are supported in the following browsers:
  • Mobile Safari and UIWebViews on iPhone and iPad, iOS 4.2 and later
  • Android Browser, Google Chrome, and WebViews, Android 3.0 and later
  • All relatively recent releases of Chrome, Firefox, Opera, and Safari
  • IE 9 and above
An exhaustive compatibility list can be found at http://caniuse.com/svg-css.

The most straightforward way to add an SVG image is to create a file with contents such as:
   <svg width="113" height="113" xmlns="http://www.w3.org/2000/svg">
     <g>
       <circle fill="#A7A6A6" cx="55.892" cy="55.893" r="55.892"/>
       <line fill="none" stroke="#FFFFFF" stroke-width="11.2" stroke-linecap="square" stroke-miterlimit="10" x1="40.125" y1="40.125" x2="71.661" y2="71.662"/>
       <line fill="none" stroke="#FFFFFF" stroke-width="11.2" stroke-linecap="square" stroke-miterlimit="10" x1="40.125" y1="71.661" x2="71.663" y2="40.125"/>
     </g>
   </svg>
and then reference that file from CSS like:
.close_gray
{ 
  background-image: url("/svg/mobile/close_gray.svg"); 
  width:22px; 
  height:22px; 
}
However, if many SVG images need to be referenced in this manner, the page loads each SVG image as a separate resource, so the previous performance gain from icon spriting is lost.

Luckily, the SVG content can be defined inline within the CSS file, such that all of the image content for the entire page gets defined within CSS. Such a definition looks something like:
.close_gray 
{ 
  background-image: url("data:image/svg+xml;utf8,<svg width='113'
height='113' xmlns='http://www.w3.org/2000/svg'> <g>  
<circle fill='#A7A6A6' cx='55.8' cy='55.8' r='55.8'/> <line 
fill='none' stroke='#fff' stroke-width='11.2' stroke-linecap='square' 
stroke-miterlimit='10' x1='40.1' y1='40.1' x2='71.6' y2='71.6'/> 
<line fill='none' stroke='#fff' stroke-width='11.2' 
stroke-linecap='square' stroke-miterlimit='10' x1='40.1' y1='71.6' 
x2='71.6' y2='40.1'/> </g></svg> "); 
  width:22px; 
  height:22px; 
}

Chrome, Safari, and IE 9+ support definition of SVG content inline with a plain-text encoding, but Firefox does not. For Firefox, base64 inline encoding works:
.close_gray 
{ 
  background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMTEzJyBoZWlnaHQ9JzExMy
cgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz4gPGc+ICA8Y2lyY2xlIGZ
pbGw9JyNBN0E2QTYnIGN4PSc1NS44JyBjeT0nNTUuOCcgcj0nNTUuOCcvPiA8bGluZSBm
aWxsPSdub25lJyBzdHJva2U9JyNmZmYnIHN0cm9rZS13aWR0aD0nMTEuMicgc3Ryb2tlL
WxpbmVjYXA9J3NxdWFyZScgc3Ryb2tlLW1pdGVybGltaXQ9JzEwJyB4MT0nNDAuMScgeT
E9JzQwLjEnIHgyPSc3MS42JyB5Mj0nNzEuNicvPiA8bGluZSBmaWxsPSdub25lJyBzdHJ
va2U9JyNmZmYnIHN0cm9rZS13aWR0aD0nMTEuMicgc3Ryb2tlLWxpbmVjYXA9J3NxdWFy
ZScgc3Ryb2tlLW1pdGVybGltaXQ9JzEwJyB4MT0nNDAuMScgeTE9JzcxLjYnIHgyPSc3M
S42JyB5Mj0nNDAuMScvPiA8L2c+PC9zdmc+IA=="); 
  width:22px; 
  height:22px; 
}
Base64 encoding takes up considerably more space. For TripAdvisor’s mobile site, inline SVG content takes up 119 KB in base64, but only 91 KB in plain-text encoding. So, do not use base64 except for browsers which require it. By defining all of our SVG images inline, we not only have retina-optimized the images, but also have removed one extra resource load at page load time- the PNG file for the icon sprite.


SVG compression techniques


Despite being vector-based, SVG files can still be quite large if one is not careful to optimize their size. Consider the following typical SVG file:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:cc="http://creativecommons.org/ns#"
  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
  xmlns:svg="http://www.w3.org/2000/svg"
  xmlns="http://www.w3.org/2000/svg"
  version="1.1"
  width="23"
  height="23"
  id="svg2">
 <defs
    id="defs4" />
 <metadata
    id="metadata7">
   <rdf:RDF>
     <cc:Work
        rdf:about="">
       <dc:format>image/svg+xml</dc:format>
       <dc:type
          rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
       <dc:title></dc:title>
     </cc:Work>
   </rdf:RDF>
 </metadata>
 <g
    transform="translate(-293.5,-470.86218)"
    id="layer1">
   <path
      d="m 315,482.36218 a 10,10 0 1 1 -20,0 10,10 0 1 1 20,0 z"
      id="path2990"
      style="fill:#8EA460;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none" />
   <path
      d="m 301.51425,477.20977 6.97149,5.15241 -6.97149,5.15241"
      id="path3760"
      style="fill:#ffffff;fill-opacity:0;stroke:#ffffff;stroke-width:2.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
 </g>
</svg>
This file is much larger than it needs to be for the browser to interpret it properly. Some tags and attributes that can be stripped out completely without affecting rendering are:
  • metadata tag
  • defs tag
  • Most of the xmlns attributes
  • Any vendor-specific SVG attributes that were inserted by a proprietary SVG file editor, but do not apply when the SVG is opened in other programs.
Furthermore, note how the numbers in the path have a precision of 5 decimal places. This is the default for many SVG editors, but this level of precision is not necessary in most cases. Rounding these numbers makes a huge difference in more complicated SVG files. We have found that we can round to a single decimal point of precision without introducing display errors in all but a few of our SVG files.

By applying these techniques to the SVG files on TripAdvisor's mobile website, we greatly reduced the size of the combined CSS file:

SVG file without these techniques, uncompressed122.5 KB
SVG file without these techniques, after gzip compression31.1 KB
Removed SVG tags and rounding, uncompressed89.2 KB27% reduction
Removed SVG tags and rounding, after gzip compression19.5 KB37% reduction


Fallback techniques for older browsers


As mentioned above, this SVG spriting technique does not work for browsers that are not listed in this support matrix: http://caniuse.com/svg-css. A traditional icon sprite must be used for older browsers.
At TripAdvisor, we use the Apache Batik library to automatically iterate through each of our SVG images and generate PNG files for each one. Then, we generate a traditional icon sprite from the generated PNG files, and set up the CSS in such a way that browsers which support SVGs use the inline SVG content, while browsers which do not fall back to the icon sprite instead.
If a browser does not support SVG, we simply add the “nosvg” class to the document body, and then the following CSS rules override the inline SVG content to instead display the icon sprite:
 .svgico 
 { 
   background-position:center center; 
   background-repeat:no-repeat; 
   background-size:contain; 
 }

 .nosvg .svgico 
 { 
   background-image: url("/sprites/mobile_pack_nosvg_1.png"); 
   background-size:auto auto; 
 }

Regardless of whether an image is displayed as an SVG or as a portion of the icon sprite, its width and height must be set to the same values. For the standard .svgico rule, background-size:contain indicates that the entire SVG contents should shrink to fit the width and height at which the image should display. However, the entire icon sprite should not shrink to the width and height of each image within the sprite, so background-size:auto is used for that case.


Alternate techniques for SVG spriting

Others have discussed a technique for SVG spriting that involves specifying all images in a single SVG file, each of them with a specific ID. The SVG has an inline CSS definition that looks something like:

 svg .image { display: none }       
 svg .image:target { display: inline }
and multiple elements, each of class image, such as:
 <g id=”selected_image” class=”image”>…</g>
 <g id=”other_image” class=”image”>…</g>
Then, CSS to show a single image within the SVG file looks like:
 background-image: url(sprite.svg#selected_image)
Since #selected_image is the current target, only it appears, while the others do not appear.
Unfortunately, this technique is only fully supported in Firefox at this time.



Overall effect on page load


The following is TripAdvisor's mobile homepage with all of our SVG optimizations applied.
The page contains no PNGs or JPGs whatsoever. The HTML markup, a single CSS file, and a single JavaScript file are loaded. A few additional resources do get ajax'd in later on from Javascript, but do not block page load. The small number of resource requests allows the DOMContentLoaded event to fire quickly. With a regular icon sprite, the time between DOMContentLoaded and load is spent on network I/O to load the images for the page. On TripAdvisor's mobile site, the time interval between the DOMContentLoaded and the onload event is minimal because those images are already embedded locally in the CSS.

It is true that DOMContentLoaded is delayed somewhat because all of the image content is in the CSS file, making that CSS file a larger resource load than it would be otherwise. In practice, particularly on mobile, the number of resource requests matters far more than the size of those resource requests. Also, SVG content compresses well and does not take up much space to begin with, and so the delay of DOMContentLoaded is negligible.

Overall, we have found that inlining our SVGs within CSS works well for creating icon sprites that look great at any screen pixel density. When possible, encoding the SVGs in CSS as plain text works the best, but for other browsers, base 64 inline encoding works well too, with the tradeoff of increased file size.

1 comment:

  1. This is awesome! Is this still the way TripAdvisor mobile renders images?

    ReplyDelete