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.