When making a web page load faster, one of the most commonly cited front-end techniques is minimizing the number of HTTP requests. In fact, it's Yahoo!'s number one rule. Each HTTP request is slow; making many requests is very slow.
Cutting down on CSS sprites
Our homepage is a collection of image-based links. Each link shows one image as its normal state, and another image as its hover state. The easy, obvious implementation is to have two image files per link.
However, this page had eighteen image-based links, which meant thirty-six separate image files. That means thirty-six HTTP requests—that's way too many.
The most popular technique for managing image-based links is CSS sprites. Rather than having two separate image files per link, instead combine the two states into a single image file, and use CSS to display the appropriate half when the user hovers over that link. This technique can cut your number of image files—and HTTP requests—in half.
When we built the second version of our homepage, we used CSS sprites for each section—the links to our portfolio, Twitter account, contact info, and more. Each link used a sprite image like this:
Fig. 1: The Mint Digital logo. Normal state on the left, hover state on the right.
Whenever someone loaded that page, with its eighteen image-based links, the browser made about eighteen HTTP requests to fetch the required sprite images. Half as many requests as before, but we still could do better.
The latest version of our homepage features not only a refreshed design, but a complete CSS rewrite. It still has eighteen separate links, and still uses the CSS sprite technique, but with only two images. Shiny. But how?
Here are the two images we use:
Fig. 2: The two image files for the new homepage. Normal state on the left, hover state on the right.
Our technique is conceptually similar to image maps, in which you use coordinates to define clickable areas of an image. However, unlike plain old image maps, we have semantic HTML and attractive hover states.
The HTML is structured like this:
<div class="about">
<p id="about-methodology">
<a href="/about/methodology">Real life is complex.
Good software is simple. Our methodology bridges
the gap</a>
</p>
<p id="about-blog">
<a href="/blog">Mint blog</a>
</p>
<p id="about-people">
<a href="/people">Mint is a mix of software
developers, interaction designs, and ideas
people</a>
</p>
<!-- ...more links... -->
</div>
Note that, even though the page is image-based, the HTML is semantic so that the page remains readable even if CSS fails to load, or if you're using a screen reader.
In the CSS, we first set the wrapper to use the normal state image as its background.
div.about {
position: relative;
width: 741px; /* Width of about.gif */
height: 681px; /* Height of about.gif */
margin: 0 auto;
background: transparent url(/images/about/about.gif)
0 0 no-repeat;
}
Next, we set each link to use the hover state image as its background.
div.about a {
background: transparent
url(/images/about/about-hover.gif)
9999em 9999em no-repeat;
text-indent: -9999em;
}
That's it for images—just these two blocks. Every link uses about-hover.gif, so the browser can reuse it for each link without re-requesting it from the server.
Note that every link is invisible by default: each link's background is positioned far beyond its bottom right corner (background-position: 9999em 9999em), and its text is positioned far beyond its left edge (text-indent: -9999em).
Now, each paragraph must be positioned. First, some global properties:
div.about p {
position: absolute;
margin: 0;
padding: 0;
}
Next, we position each individual paragraph to line up with its clickable area. Here's an example for p#about-methodology:
p#about-methodology {
left: 180px;
top: 326px;
width: 337px;
height: 90px;
}
To make sure that the paragraph is positioned correctly, it's helpful to temporarily add debug code like outline: 1px solid #0f0 to the CSS, which looks like this:
Fig. 3: Highlighting an individual link for debugging.
Let's revisit the global CSS for each link. We want each link to fill the entire area of its parent paragraph, so we extend it to this:
div.about a {
display: block; /* Fills parent's width */
height: 100%; /* Fills parent's height */
overflow: hidden;
background: transparent
url(/images/about/about-hover.gif)
9999em 9999em no-repeat;
text-indent: -9999em;
}
Lastly, we want to define the hover state for each link. So, we extend the CSS for p#about-methodology to this:
p#about-methodology {
left: 180px;
top: 326px;
width: 337px;
height: 90px;
}
p#about-methodology a:hover,
p#about-methodology a:focus,
p#about-methodology a:active {
background-position: -180px -326px;
}
The background-position value for the link is simply the negative of its paragraph's position. This shows exactly the portion of its background—the hover image—that's needed. Easy!
(N.B.: We could have merged about.gif and about-hover.gif to cut it down to only one HTTP request. However, this would have made it less convenient to set and maintain the background coordinates for each hover state.)
Here's an overview of the CSS:
/* For the whole section: */
div.about {
position: relative;
width: 741px; /* Width of about.gif */
height: 681px; /* Height of about.gif */
margin: 0 auto;
background: transparent
url(/images/about/about.gif)
0 0 no-repeat;
}
div.about p {
position: absolute;
margin: 0;
padding: 0;
}
div.about a {
display: block;
height: 100%;
overflow: hidden;
background: transparent
url(/images/about/about-hover.gif)
9999em 9999em no-repeat;
text-indent: -9999em;
}
/* For each link: */
p#about-methodology {
left: 180px;
top: 326px;
width: 337px;
height: 90px;
}
p#about-methodology a:hover,
p#about-methodology a:focus,
p#about-methodology a:active {
background-position: -180px -326px;
}
From here, we just add another block of CSS to position each paragraph/link, similar to that of p#about-methodology.
In the end, instead of eighteen separate sprite images, we now use just two. We cut image HTTP requests by 89%. We're pretty pleased with the results all around.
Comments
Keith Chu
09 December, 2009
Very nice job, team!
One question though: why reference each of the sections in .about by ID? ".about .methodology" seems pretty clear. Unless it's a JS hook.
Ron DeVera
09 December, 2009
Thanks Keith! We typically do avoid IDs on sites in development, since classes can be easily reused without code conflicts.
Here, I'd say both approaches work well. I chose IDs because I didn't foresee any good reason to reuse class names, and wanted the code to reflect that expectation.
Nathan Fisher
31 March, 2010
Looks good, have you considered using attribute selectors? Gets rid of the encapsulating paragraph tag, which often carry little semantic meaning. Works in many mobile browsers, only caveat is IE6, but what else is new. :(
a[href="/about/methodology"] { background-position: -180px -326px; }