We are constantly looking for ways to make our products faster so recently we spent some time optimizing UI graphics in Basecamp. With better support for CSS3 properties in the latest browsers and solid techniques for progressive enhancement, we began by eliminating some graphics altogether. We found that subtle gradients and drop shadows can be completely rendered with CSS properties and many times aren’t missed when viewed with a browser that doesn’t support them — the very definition of progressive enhancement. Using CSS instead of these graphics results in fewer HTTP requests to our servers plus browsers draw native CSS elements much faster than images.
Another approach we’ve used is CSS sprites, a method for combining many graphics into a single image which is then displayed via CSS. For us this technique reduced dozens of HTTP requests into one — a single, cache-friendly image file. For those new to the technique, the stylesheet references the coordinates of the desired graphic inside the image file. But keeping track of coordinates and creating new CSS styles everytime we wanted to use a graphic would have added a lot of code and made maintenance a chore.
We wanted to keep the code as easy to write as the Rails image_tag
method that we used previously. So this:
<%= image_tag ("email.gif"), :class => "email" %>
…became this:
<%= image_sprite :email, :class => "email", :title => "Email" %>
The image_sprite
helper method contains the dimensions and coordinates for each and renders the HTML. Here’s a shortened look at the method:
def image_sprite(image, options = {})
sprites = {
:add_icon => {:w => 16, :h => 16, :x => 0, :y => 0},
:email => {:w => 26, :h => 16, :x => 41, :y => 0},
:print => {:w => 25, :h => 17, :x => 68, :y => 0},
:trash => {:w => 10, :h => 11, :x => 94, :y => 0},
:comments => {:w => 13, :h => 13, :x => 105, :y => 0},
:comments_read => {:w => 13, :h => 13, :x => 120, :y => 0},
:comments_unread => {:w => 13, :h => 13, :x => 135, :y => 0},
:rss => {:w => 14, :h => 14, :x => 150, :y => 0},
:ical => {:w => 14, :h => 16, :x => 166, :y => 0},
:drag => {:w => 11, :h => 11, :x => 360, :y => 0},
:timeclock => {:w => 17, :h => 17, :x => 375, :y => 0},
:timeclock_off => {:w => 17, :h => 17, :x => 392, :y => 0}
}
%(<span class="sprite #{options[:class]}" style="background: url(#{path_to_image('/images/basecamp_sprites.png')}) no-repeat -#{sprites[image][:x]}px -#{sprites[image][:y]}px; width: #{sprites[image][:w]}px; padding-top: #{sprites[image][:h]}px; #{options[:style]}" title="#{options[:title]}">#{options[:title]}</span>)
end
Keeping the image details in a helper method made it easy to convert the existing images to sprites, easy to re-use the sprited images throughout the app, and will really pay off anytime there are changes to the images. Update the sprite image, update the helper, and the change is done everywhere. This improvement has already been rolled out for Basecamp and we hope to streamline our other products using these techniques soon.
Jim Gay
on 25 May 10Is there a reason you didn’t just use image_sprite :email and have the helper make some default guesses about what’s required?
Damon
on 25 May 10Why not just use your external style sheet to define the background and placement, etc. that you are using inline? You’ll still just have to edit the images and one file.
JZ
on 25 May 10Jim, :class and :title are optional parameters. Sure, in some cases they might match (like this example) but in many they do not.
Because we were converting existing images in the app to sprites it was less friction to preserve any class names, and keeping them in the view meant we didn’t have to go back to the helper to look up which class will render when tweaking the CSS.
But you’re right, if I was starting from scratch on a new app I could have come up with a good system with predictable defaults and let the helper handle that.
JZ
on 25 May 10Damon, there are a couple of advantages to this method:
1. Because it is in the helper we can globally update the mark-up should we need additional attributes or maybe come up with a better technique. For example, I’m not crazy about using a SPAN element here. I can foresee us revisiting this at some point.
2. Personally, I like the tabular format of setting the variables in Rails. It’s compact, easy to read, understand, and maintain. Keep in mind, that’s just a portion of the method, we’ve got dozens of sprited images — it would take a lot of lines in CSS to define all of those.
Lance Leonard
on 25 May 10I get the usage behind this, but I see a couple negatives to the specific implementation. Granted, bandwidth is pretty moot these days sans mobile delivery, but writing out inline styles will increase the page weight each time it’s used, rather than using classes + CSS with Damon’s suggestion (sorry for pointing out the obvious). Maybe there’s a more hybrid approach in there somewhere.
In addition, I’ve never really grasped things like HAML and Sass – using server-side tech to simplify client-side results. Obviously, that’s a very general statement, and I don’t want to confuse the intent since that’s the point of web frameworks. I completely understand the need (CSS variables, etc), but it just “feels” dirty to step back to blurring the line between the separation of functionality of data, business and presentation logic and server and client-side functionality. This feels like a very similar approach using server-side helpers for something that could be entirely client-side (as opposed to reformatting vars, etc).
riddle
on 25 May 10Spriting is such a great and efficient technique I feel like a jackass pointing this out, but you need some alternative way of putting text there.
Because right now to make things really simple, you output a span set up to behave & look like an image. If images are not downloaded or somehow not supported (it happens on mobile), user looks into a void (padding-top pushes text off the element to be clipped outside).
Previously you used alt attribute (or did you?). So I suggest using small placeholder image (let’s say transparent png) with alternate text or Glider/Levin IR.
Accessibility is not only for nerds. :)
Jeff
on 25 May 10Interesting post. I’m a big time follower of 37signals and have tried to learn everything I can from rework and SVN.
I just started my first company and after trying to create a decent front-end I thought it be best to have a professional. Can any one point me in the direction of a good designer? [email protected]
Joe Sak
on 25 May 10Awesome, reminds me of this little gem I’ve been meaning to finish http://github.com/joemsak/LemonLime It takes some data, such as link names, widths & heights, hover & active positions, and then writes HTML & CSS for you. I got it returning HTML but haven’t finished it yet.
Joe Sak
on 25 May 10Sorry, didn’t know I had to link manually: http://github.com/joemsak/LemonLime—used to URLs being auto-linked in comment systems…
Big Time Follower
on 25 May 10@Jeff… come on!? Big time follower, huh? Head over to the 37Signals Sortfolio, yo! http://sortfolio.com/
Omarvelous
on 25 May 10I will have to agree… I feel CSS is the better way to go… With the latest Compass, and their built in Sprite support, it makes it even easier… I was able to sprite up all the FamFamFam Silk Sprites… that’s about 400+
Parker Selbert
on 25 May 10@lance – As I’m sure you can guess this technique works just as well, or even better in css/sass markup, under the same principal. The images they are using in this example are the content, and wouldn’t make much sense as css backgrounds, so I don’t see the line between business and presentation taking place.
A great way to bypass *n asset subdomains!
Anders
on 25 May 10Readers might also be interested in Jammit which handles embedding images referenced in CSS (Data-URI / MHTML image embedding). I was personally impressed by how easily Jammit handled CSS/JS minifying + CSS image embedding for us (http requests per page went down with about 80%).
Alex Soulim
on 26 May 10Why don’t use just HTML and CSS? I mean defining sprites in CSS.
Like example: CSS .sprite { width:10px; padding-top:10px; } .email { background: url(/images/basecamp_sprites.png) no-repeat -10px -10px; } HTMLtitle
Sean McCambridge
on 26 May 10I agree with Alex. What’s with the abstraction and obfuscation? Just learn CSS. It’s so much simpler. I guess a JavaScript guru would say the same about JS frameworks. But additional layers like HAML and SASS don’t seem to make things easier or smaller - just code-ier and harder to read - and they let you eff around and make things extra complicated where good CSS should be simple and lean.
riddle
on 26 May 10I personally don’t like SASS, but HAML is a time saver.
Bart Teeuwisse
on 26 May 10Jason, have you seen the ImageBundle plugin? ImageBundle creates both the sprite and the CSS for you. ImageBundle adds 1 helper method: image_bundle that is concise and flexible. Have a look and let us know what you think.
JZ
on 26 May 10I hadn’t seen that before, Bart. Very cool.
In fact we remarked at one point during the project that the next logical step would be for rails to automatically create the sprite and populate the helper coordinates.
We’ll check out your implementation. Thanks!
Anonymous Coward
on 31 May 10asda
This discussion is closed.