CSS Images and Data URIs
A few weeks ago I took a quick look at replacing the CSS images on this site with data URIs, via Nicholas Zakas’ handy CSSEmbed tool. I figured out I could serve the same amount of data with 1 HTTP request instead of 14. At face value it sounded like a huge performance gain: same amount of data transmitted, far fewer HTTP requests, how could it not result in significantly better performance?
That’s why the results I encountered when testing out the concept on a recent project were so surprising. Using data URIs for CSS background images actually slowed down the page. Edit: To be clear, the negative performance effect I describe is particular to the conditions of my test, namely, a situation in which one or more normal CSS images are served alongside data URIs. I’m not suggesting that using data URIs for CSS images results in poor performance in general.
My specimen for the experiment was a small brochure site I’d recently designed. The original version required 20 HTTP requests with a total page load of 200K. The test version with the data URIs required 6 HTTP requests and had the same load size of 200K.
To implement the data URIs I set up three stylesheets:
- The original stylesheet, but with all the background image references removed;
- A stylesheet containing only the data URI CSS images;
- A stylesheet containing only the normal background image declarations.
The idea is that all browsers would get stylesheet #1, browsers that support data URIs (FF, Safari, Chrome, IE8, Opera) would get stylesheet #2, and IE6/7 would get stylesheet #3 with the normal image references.
So instead of trying to use MHTML or other trickery to coerce IE6/7 into reading data URIs, we simply decide that only the “modern browsers” get to play.
In addition to allowing us to work around non-compliant browsers, isolating the data URIs in their own file achieves two things. One, it keeps our main CSS clean and more maintainable. Two, it allows us to cache the images separately from the styles, so that we can make a simple edit to the main stylesheet without forcing users to re-download all of the image data.
The 6 HTTP requests for my test version consisted of 1 HTML, 2 JS, 2 CSS, and 1 background image (80K) that was too large to serve as a data URI. As we’ll see in a minute, this one image turns out to be a performance killer.
The first waterfall is the original page and the second is the page with the data URIs (both taken from Firefox 3.6 beta, empty cache). Even though the payload size is the same, the version with 14 more HTTP requests loads about 18% faster. We appear to be in violation of one of the laws of web performance physics.
If you have a lot of experience trying to make sense of these waterfall charts the problem should be immediately clear. In this example it’s not about load size or number of requests, but parallel downloads.
It appears that images referenced in a stylesheet are only downloaded by the browser once all stylesheets have finished downloading. (I don’t remember noticing this before, or it being mentioned in either of Souder’s books, but I’m assuming it’s a well-known fact.) Edit: For more on this, see the addendum at the end of the post.
This creates a problem if we have any images that are too large to serve as data URIs. We can expect our data URI CSS file to be one of the larger page components, finishing its download late in the sequence even if initiated early. Any remaining CSS images, which will necessarily be on the larger size (32Kb+), will have to stand by waiting for the data URI CSS to finish downloading.
In this case, where we have only one of these large images, the image is downloaded all by itself, completely wasting the browser’s ability to download components concurrently.
Lessons (What’s an HTTP request optimizer to do?)
If you’re able to convert all of your background images to data URIs you’ll avoid the aforementioned trouble. But if you have even one CSS image that must be served regularly you’re likely to encounter a performance penalty. The degree of the penalty depends on the size of your data URI CSS file, the size of the remaining image(s), and how many other components your page contains. If the data URI file finishes up relatively early, while the browser is still downloading JS and inline images, the penalty will probably go unnoticed. If the data URI file finishes late, followed by a large image, then the negative effect is likely to be felt.
In some cases it may be possible to change a large background image to an inline image, enabling it to be downloaded independently of the stylesheets.
Looking at the above waterfall chart I began to wonder about an HTTP request sweet spot number, beneath which performance gains would be negated or even reversed. Modern browsers can easily handle 10 or more simultaneous requests. Are we working against the browser if we chunk the payload into 3 or 4 heavy files?
data URI Resources
- Data URIs Explained (Nicholas Zakas breaks down how data URIs work)
- CSSEmbed (Command line tool for converting images to data URIs)
- Data URI Sprites (Online tool for generating data URI stylesheets)
- MHTML – when you need data: URIs in IE7 and under (Stoyan Stefanov explains how to achieve data URIs in IE7 and below using MIME HTML)
I whipped together a test page for looking at how browsers download images referenced in stylesheets. So far: In FF 3.5 and Safari 4, the browser will wait to start downloading CSS images until all external stylesheets have been downloaded, even if some of the stylesheets are placed at the bottom of the page. In IE7, the browser will wait only for stylesheets placed in the HEAD section.
Now that I think about it, this makes perfect sense. And I think it may have been touched on in High Performance Web Sites after all.