Far Future Expires Headers for CSS Images in Rails

Posted on 28 Sep 2009

Setting far future expires headers for your static assets noticeably improves the speed at which pages load and is easy to setup in Rails thanks to asset timestamps. All you need to do is:

  1. Use image_tag, stylesheet_link_tag, javascript_include_tag and friends in your views.
  2. Update your web server config to set the correct headers.

One problem that's not immediately obvious here is that while your web server is happily adding expires headers to the images you're using in CSS, those requests don't have the timestamp in the query string so the browser will continue to use the cached version after you have updated the file.

One solution is to instruct the web server to only add the expires header to requests that do include the asset timestamp. While this might be ok if you only have a few images in your CSS, if most of your images are specified this way then you're still not making the most of the browser's cache which was the whole point of the exercise in the first place.

An alternative solution is to use ERB templates to generate your stylesheets, and use the Rails' helpers to get asset timestamps in your CSS. It's dead easy, here's how:

Create a Stylesheets Controller and a Matching Route

class StylesheetsController < ApplicationController
  caches_page :application, :iphone
end
map.connect "/stylesheets/:action.css", :controller => "stylesheets", :format => "css"

This is just about as simple as you can imagine. We're using page caching to cache the stylesheet to disk, once rendered it will be served like any other static asset.

Move Stylesheets to the Views Directory

mv public/stylesheets/application.css app/views/stylesheets/application.css.erb
mv public/stylesheets/iphone.css app/views/stylesheets/iphone.css.erb

Use the image_path Helper

Wherever you reference an image in your stylesheet, use the image_path helper like so:

body { background: url(<%= image_path('fade.png') %>); }

When the template renders this will output something like:

body { background: url(/images/fade.png?1253089219); }

And that's all there is to it. After each deploy the cached stylesheet will disappear and the next time it's requested it will be regenerated with new timestamps, which in turn will cause the browser to fetch fresh copies of your images.

A Note on Asset Hosts

We're also using an asset host, for a couple of reasons:

  1. A separate host for static assets can itself improve page load speed.
  2. We can specify far-future headers on anything served from the asset host, safe in the knowledge that the URL will have been generated using the Rails helpers and will therefore include the timestamp in the query string.

This causes one additional complication with the approach described earlier however. Our asset host doesn't run Rails since it only serves static assets, but the first request for the stylesheet needs to go through Rails so the template can be rendered and written to disk.

Assuming you're using Capistrano, a simple solution is to add a task that fetches the stylesheets from the Rails host after each deploy. Adding this to deploy.rb will do the trick:

task :prime_cache, :roles => :app do
  run <<-CMD
    wget --spider http://#{domain}/stylesheets/application.css;
    wget --spider http://#{domain}/stylesheets/iphone.css
  CMD
end

after "deploy", :prime_cache
after "deploy:migrations", :prime_cache