The images, css files and javascript files in a web application, the “static resources”, are a big part of each request, both in number and in bandwidth. That’s why they they should be cached by the browser for as long as possible. By setting a HTTP-EXPIRES header in the far future (at least a year from now) the web server tells the web browser that it’s ok to cache these resources, saving us a lot of bandwidth and improving the page load time for the user.
But sometimes we want to make a change in one of our resources, and want the browser to update the resource at once, long before the cached version expires. To do this we must create a new URL for the resource. Yahoo solves this in their YUI project by giving their css resources a version number:
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.2/build/reset/reset-min.css"/>
This is a fine solution, but it involves some overhead – if you just want to use the latest version, you have to update the version number in all places you include the resource.
I’ve invented another trick which works fine as long as we, unlike Yahoo, don’t care about supporting older versions of resources. To generate the URL of a resource, we call a function, e.g. versioned_css("main"). In this function we append the git hash (git-hash-object filename) to the filename, before the extension, caching the result (because we don’t want to calculate the hash more than once). The first ten digits of the git hash will be sufficiently long to be unique (a git hash consists of 40 hexadecimal digits). Also, there is no need to use git – there are other ways to create a unique hash from a file.
Whenever the file changes, its hash will also change, creating a new and uncached URL, which is exactly the effect we’re looking for.
However, we still need to map the URL-with-hash to our resource, a file named something like “main.css”. We could add the hash to the filename, by renaming the resource to “main-c0a9a0a5ec.css” during the deployment of the application, but it is easier to have Apache remove the hash using url-rewriting:
RewriteRule ^/stylesheets/(.+)-[0-9a-f]{10}\.css$ \
http://127.0.0.1:3010/stylesheets/$1.css [P,QSA,L]
The difference with renaming the resource is that with rewriting a request for “main.css” with any hash at all will return the latest version of the file, while with renaming an incorrect hash would lead to a 404 error.
The strength of this trick is that it lets you use far-future expire dates, is easy to implement and allows you to forget about it afterwards, particularly if you extend the built-in methods in Rails to generate urls for resources.
July 27, 2008 at 4:07 am
What about having version in query string?
July 27, 2008 at 7:53 am
Theres an easier way to do static file caching… Save the filename as is “whatever.js”. Now to do the versioning you append a “?” on the end and add the file modification date. So you end up with whatever.js?1217141507. Now whenever you make a change the version is automatically updated.
July 27, 2008 at 7:54 am
So why use a rewrite rule at all? Seems that href=”/css/main.css?c0a9a0a5ec” would do just as well.
July 27, 2008 at 9:42 am
Using other mechanisms but expiry time might suffice for most people. See ETags and partial requests.
July 27, 2008 at 1:19 pm
@Moshe & Adam: That’s what Rails does, and it works (as long as you have an expires-tag, otherwise it’s useless). I prefer hashes to timestamps (with multiple web-servers timestamps may not be identical) and have an irrational dislike for using the query-string like this.
@Reggie: Yes, the rewrite rule has essentially the same effect.
@a: ETags are used for cache validation, i.e. you don’t know if your cached object is fresh, and the web server returns a 304 if it is by comparing the Etags. So this will save bandwidth, but not reduce the number of requests.
July 28, 2008 at 3:32 pm
[...] A Trick For Caching And Expiring Static Web Resources we call a function, e.g. versioned_css(”main”). In this function we append the git hash (git-hash-object filename) to the filename, before the extension, caching the result (tags: caching web) [...]