Asset Versioning in Rails
<script src="/javascripts/jquery.js?1212761793" type="text/javascript"></script>
"What are those numbers Rails puts after my image, css, and javascript files?"
— Some Rails developer
That's the time (cast to an integer) that the file was last modified. Rails tags assets with a timestamp to solve potential issues with browser caching and to improve page load time.
Whenever the file changes, the query string will change, causing browsers to download a fresh version of the asset instead of using a cached copy. This ensures that users will never be using an old css/js/image file. Because the query string will change any time the file changes, you can set a far future expires header, telling the browser that it can cache the asset indefinitely.
Rails takes care of the query string, but you need to set up the expires header in your web server config.
Setting a Far Future Expires Header
The Rails documentation provides the following sample configuration to set the expires header in Apache.
ExpiresActive On <FilesMatch "\.(ico|gif|jpe?g|png|js|css)$"> ExpiresDefault "access plus 1 year" </FilesMatch>
But there could be a problem with setting the expires header this way. If you have an image/css/js file that does not have a version, when you update that file, your users will still be using the old copy that's cached in their browser. Because the response header told the browser to cache the file as long as it wants to, it could be a while before the user gets the new version.
You really only want to set the expires header when serving up versioned assets. Here's one way to do that with Apache.
RewriteCond %{REQUEST_URI} (css|js|jpe?g|png|gif|ico)$ [NC]
RewriteCond %{QUERY_STRING} ^[0-9]+$
RewriteRule ^(.*)$ $1 [E=set_expires_header:true,L]
Header add Expires "Wed, 04 Aug 2010 16:46:21 -0500" env=set_expires_header
I'm not an Apache guru, so there may be a more elegant way to do this. Also, on the project where I'm doing this, the Apache config file is generated. We set the expires header to 2 years from the time of the file generation. I don't know of a way to dynamically set the date otherwise.
We can use curl to check that requesting the image with the asset version will set the expires header.
$ curl --head http://localhost:9999/images/rails.png?1213412 HTTP/1.1 200 OK Date: Fri, 05 Sep 2008 02:34:25 GMT Server: Apache/2.2.8 (Unix) mod_ssl/2.2.8 OpenSSL/0.9.7l Phusion_Passenger/2.0.2 Last-Modified: Thu, 04 Sep 2008 13:40:13 GMT Accept-Ranges: bytes Content-Length: 6646 Expires: Wed, 04 Aug 2010 16:46:21 -0500 Content-Type: image/png
And if we request the image without the version, there's no expires header.
$ curl --head http://localhost:9999/images/rails.png HTTP/1.1 200 OK Date: Fri, 05 Sep 2008 02:34:13 GMT Server: Apache/2.2.8 (Unix) mod_ssl/2.2.8 OpenSSL/0.9.7l Phusion_Passenger/2.0.2 Last-Modified: Thu, 04 Sep 2008 13:40:13 GMT Accept-Ranges: bytes Content-Length: 6646 Content-Type: image/png
Forcing the Browser to Update Its Cache
You want to make sure that whenever you update your asset files, your users aren't using a stale cached copy. Rails takes care of this, because whenever the query string after the file changes, the browser will treat it like a new file and download it. I read that (according to the HTTP spec) Safari won't cache files that use a query string, but I did some testing with Safari 3.1.2 on OS X and that doesn't appear to be true.
The only thing that you have to do to update the query string is modify and file and possibly restart your app server. As of Rails 2.1, the query string won't change unless you restart your server. Older versions of Rails will update it without the restart. In practice, the Rails 2.1 behavior is fine since you only update files when doing deployments, and you're restarting your server then anyway.
Even if you're not setting the far-future expires header, it's a good idea to version your assets. Browsers will cache files without any expires header for a while, and some users are going to be behind proxies that have their own crazy caching strategy.
There's a few more good practices with asset versioning that Rails doesn't do for you - I'll be blogging about them soon.
Posted on 2008-09-06 | permalink | del.icio.us
Blog Archive
