Data URI Sprites

In the inaugural topic of this blog on site speed, Hugh Williams spoke briefly about image spriting as a technique to speed up the rendering of a page. CSS Image Sprites are now commonly used to remove the performance problems created by making separate HTTP requests for every image on a web page. Unfortunately the technique cannot be used on a web page with a dynamic image set such as the eBay search results page: the images on the page change with every request based on the item specifics, therefore combining them into one sprite image is not possible.

When brainstorming alternatives, we found what seemed to be an obvious solution: the image Data URI scheme. It works in all major browsers (except IE6 and IE7), and it solves our problem of too many HTTP calls. The code looks like this:

In CSS:

#item1 {
background: url(data:image/png;base64,) no-repeat;
}

Though the Data URI scheme solved our original problem, it created two new ones:

  • A much larger HTML payload, since the images are now embedded in the page
  • A loss of browser cache benefits: all images are sent over the wire on every page request

We realized what we really needed was a combination of the two approaches, because we would then be able to create image sprites dynamically. So we wrote urigen, a simple web service that generates a base64-encoded image data URI scheme response in JSON format for a given set of image URLs.

Under the hood
For a page with a dynamic image set, the browser calls the urigen web service immediately after the page load event with the image URLs passed as GET (or POST) query string parameters. The web service then extracts the image URLs from the query string, makes HTTP calls to the image hosting servers, does a base64 encoding on the response, builds a JSON array and returns it as shown below.

Request:

http://localhost/services/urigen?thumbs2.ebaystatic.com/m/mfqVqeVYnfS8MAHPgpTnINQ/140.jpg&thumbs4.ebaystatic.com/m/mZScfBs8aQzwcGokxSUV_mQ/140.jpg

Or

http://localhost/services/urigen?params={“images”:[“http://thumbs2.ebaystatic.com/m/mfqVqeVYnfS8MAHPgpTnINQ/140.jpg",”http://thumbs4.ebaystatic.com/m/mZScfBs8aQzwcGokxSUV_mQ/140.jpg”]}

Response:

{"data":[
{"url":"http:\/\/thumbs2.ebaystatic.com\/m\/home\/images\/productbrowser\/140.jpg",
"uri":"\/9j\/4AAQSkZJRgABAgAAZABkAAD\/........5AQEBAQEBAQEBAQEBAQEBAQEH\/\/z"},
{"url":"http://thumbs4.ebaystatic.com/m/mZScfBs8aQzwcGokxSUV_mQ/140.jpg",
"uri":"R0lGODlhbgAtANUAALIAJpnMAAAAmczl.......B4NmdQZTJ5QUFBQUFFbEZUa1N1UW1DQw=="}]}

The JavaScript engine then de-serializes the JSON response, creates images with the data URI scheme and displays them to the user. This technique (we call it Data URI sprites) blends image spriting with the Data URI scheme. Multiple image HTTP requests are dynamically combined into one, and there is no need to lay out images ahead of time as is necessary for CSS sprites.

Data URI sprites can be effectively used for dynamic images below the fold on a page, or for images that are shown on demand based on user actions.

A prototype of Data URI sprites with detailed documentation is available in the public github repository ImageURIGen. Websites like Flickr have tried similar techniques and have reported substantial performance gains, as Ross Harmes from the Flickr frontend engineering team explains in the High Performance JavaScript book.

Proposed optimizations:

  • The JSON response should have the appropriate HTTP cache headers to benefit from browser caching, if the same URLs are requested again
  • If the service is hosted on the same web server that is hosting the images, HTTP calls (from web service to image server) and their associated latency can be avoided and images can be read from the disk locally.
  • The web service should have a proper LRU caching mechanism in place to avoid repetitive calls for the same image and instead retrieve data from the server cache
  • The service should also be available in JSONP format with a JavaScript callback to enable browser cross domain communication
  • The urigen web service itself can be a server side call from the page and the data  sent as a separate below-fold chunk after the main page is flushed out

Senthil Padmanabhan
Engineering Lead & Site Speed Evangelist