Friday, September 26, 2008

Faster Web: CSS Sprites

I've been really busy at work so this is likely a short post (but hopefully an interesting one). Now almost all of us are using the Internet on daily basis. But a lot of the websites out there are very inefficient and slow! Now there are two ways to improve latency for these websites, one way is to tweak the backend code to be more efficient. For example, eliminate SQL JOIN and use less normalized table; yeah I know, against CS2102S principle of normalization, but guess what, table JOIN sucks! It constructs table in-memory, and if you're not careful, that table could be huge, I'm talking about INNER JOIN here, don't even ask about CROSS JOIN. Another example is to use proper INDEX in your database. Yes, all these are good, but you know what, after some point, tweaking the backend becomes a really hard problem that can take months (imagine Google or Facebook scale) to complete, only succeeding in reducing latency by a small 5-10%. The better way to improve latency is to improve frontend latency! That is how your website got into your users' web browsers, how is it being rendered, speeding up the Javascript, etc. Improving frontend latency can speed up your website by at least 20%. Just with the technique I describe here, some website could have loaded up to 50% faster (your mileage will vary though, as you'll see).

Yes, in this article, I'm going to just show one way to improve your website latency. (For more, visit Steven Souders' High Performance Web Site website or buy his book). Just one, but I assure you, it'll help you write faster website. (I actually wanted to do two, but decided to not do the other one right now.)

The rule is: reduce the number of HTTP requests.

Yes, simple rule, but guess what, almost everyone are not following this rule. Don't believe me, download Firebug extension for Firefox and use the Net tab to analyze websites. Let's just try with the website we are all familiar with: www.comp.nus.edu.sg! Yes School of Computing's website! Open the website, open Firebug, open the Net tab and enable it. Press Ctrl+Shift+R (or Cmd+Shift+R in OS X) to force cache reload (most visitors to SoC website can be assumed to be first time visitors who wanted to evaluate SoC as a choice of school). Count the number of request. Believe me already? Yes! SoC website has 42 HTTP request! Unbelievable! (Some websites and blogs can have up to 100 to 200 HTTP requests, especially those *ahem*download*ahem* websites with tonnes of ads.)

SoC website's # of HTTP request
Taken with Webkit
42 requests! That's the number of request SoC website made. The top image is taken from FF3's Firebug Net tab, which is not very accurate; the bottom picture is a more accurate representation taken from Webkit's (Safari Nightlies) resource inspector. Note the time taken to send the HTTP request (lighter colour) and the actual time taken to transfer the data (darker colour). Both are not as accurate a sniffing, but I don't have the right tool on this computer.


So how do we decrease the number of HTTP requests? If your website has tonnes of small images, the best way will be to use CSS sprites! In the past web designers slice and dice their images to create smaller images that are arranged with tables (and whatnot) to form the full images. The reason: the cable that connects the internet to home was slow! 56Kbps to download a huge 500KB image? That will take ages! But today, today is a completely different age! We now sprite images together. We combine all those small few kilobytes image into a bigger 50KB-100KB file. Why? The cable is now fat enough that negotiating TCP connection and HTTP request can be a bottleneck (remember HTTP requests contains a lot of headers, including that huge header that everyone is afraid of, Cookies!).

How does this work exactly? Well, recall some CSS. With CSS, you can set a background of a "box" (a div or an img) using background property. At the same time, you can size the box with height and width. You got the idea? Yes, set the background image to the sprited image, and size it to the size of the image that you want. That's not all, is it? Yes, that's not all. Right now, you are probably showing the top left part of the image. Now we want to adjust the background image position. No problem! We can adjust the top and left of the background image to negative numbers! Yes! Let's combine all those together shall we? For this example, let's show a rectangle that is part of the image above! The CSS I'm going to use is:

#the-sprite {
background: url('the-image.jpg') -75px -80px;
height: 80px;
width: 100px;
}

That's it? That's it! (Well, rinse and repeat for the other divs and imgs; the main idea is that you're only downloading 'the-image.jpg' instead of a lot of different images.) Let's see it in action! Here is the resulting image:



Now, I'm using a div for simplicity, but you can also use img, li, and some other thing. If you're using img however, you need to use a placeholder image as the src attribute. Maybe a 1px by 1px jpeg image (use this same image everywhere so that you only need to load it once)? Also note that while I use inline CSS for this example, it is better to set the id of the div or img and use external stylesheet.

That's way cool right? Try it the next time you design a website! A rule of thumb, a big image is better left alone. A dynamic image (e.g. image in a blog post may not appear everytime you load a blog) is better left alone too. So do this for all those static small images you use. This method is very versatile, you can even combine it with your Javascript skills to create cool stuffs like sliding images, hover menu, etc. Your imagination is your limit!

With any technique, there is always some drawback. What's the drawback of this technique? All of the sprited images will be loaded together! So before the download of the image is complete, none of the image is displayed. If your page only download one or two resources other than the HTML itself (say a stylesheet and the sprite), this method may make loading slower instead of faster. The reason is you can't parallelize the downloads. Remember that modern browsers usually download two to six resources in parallel (FF2 downloads 2 in parallel, FF3 downloads four or five). So play with your resources. Make it such that you enjoy the parallelization of downloads but at the same time minimizing the number of HTTP requests (remember, although browsers parallelize downloads, they only download a few in parallel, so if there are tens of requests...). Remember to exclude javascript from your calculation. Javascript is treated differently. The way Javascript is treated warrant another article in its own. I may not have the time to write that one up though. ):

Another drawback, spriting images are not an easy task! Furthermore, if you add new images, you have to add those images to the sprite. It's troublesome, that's why not many website use it (mind you, a lot of the larger websites use this technique!).

This technique alone could reduce SoC website's number of HTTP requests by more than 20! (I did a rough count and counted 24 static images that shouldn't change until the website got redesigned.) It will be harder to apply the techniques to blog with pictures that change all the time. It is still possible to sprite some of the static images though (e.g. buttons, navigation images).

Well, that's it for today! Hope this post helps you in some way or another the next time you design a website.

P.S. A List Apart has an article to make a navigation bar based on CSS sprites here. A friend told me that it's pretty cool (I haven't read it myself).

- Chris

4 comments:

Huy Nguyen said...

Nice written Chris,
Your article mainly talks about reduce http requests in term of using sprited images, last time I also found another article talking about combining (and compress) javascript, css files.
http://aciddrop.com/2008/01/21/boost-your-website-load-time-with-3-lines-of-code/

chris said...

Haha, yeah. Gzipping contents save a lot of bandwidth (and makes page loads much faster in slower computer). It does put more load on the server though. I'm not sure how PHP does it, but it will be great if they cache the results instead of gzipping on every request.

As to combining Javascript together, it's good to do, but not too excessive. You probably need some Javascript to render your page, but most of your Javascript will only be useful long after the page has loaded (think GMail, most of the Javascript used there will only start being useful after the entire page is loaded). So splitting into two bundles (or even more) will be pretty useful. One small one to put at <head>, another to put at the end. Remember that Javascript downloads block all other downloads in most browsers but the newest (there are ways around it, but they are not something everyone knows or wants to do).

Anonymous said...

You forgot to close the abbr tag up there.

chris said...

Oh, I think Juliana fixed it, thanks Juliana!