Automatic versioning of static content for Django sites

3 March 2008 django, performance

next: Django request logging

A standard performance tweak these days is to set long expiration dates on your site's static content. Here's the best way I've come up with to do that for my Django sites.

If you're not familiar with the basic technique, setting a "far future" Expires header is number 3 in Yahoo!'s list of best practices for speeding up your web site:

Basically, you want to be able to tell your visitors that they can keep using your CSS, JavaScript, Flash, etc. for as long as they care to keep it in their cache. This cuts way down on the number of requests you're handling, which helps you serve more visitors, each of whom has a much better experience, as pages load so much more quickly. It's beautiful.

But you also want to be able to change your content at some point, without visitors continuing to use stale stylesheets or scripts. You have to vary the content URL with a version indicator, usually a timestamp or a build number.

So instead of telling the browser that it needs to fetch /static/css/default.css, you direct it to /static/css/default.css?20080303, or better still, /20080303/static/css/default.css, which may avoid problems with less capable proxies.

How you maintain that version indicator is the key difference between applications of this technique. If you update it manually, you'll forget. If your deployment process is complex enough that you use Ant or some other scripting tool, you can easily tweak those to update your static content version. But frankly, all my Django deployments have proven so simple that I've left those contraptions behind.

So, here's how I build clean versioned URLs for static content, with no external automation, and no extra work at deployment time.

The first step is changing your Django MEDIA_URL and ADMIN_MEDIA_PREFIX settings to include a version. Easy enough:

ADMIN_MEDIA_PREFIX = '/%s/static/admin/' % REVISION
MEDIA_URL = '/%s/static/' % REVISION

But where to get REVISION? For a while I just used time.strftime("%j", time.localtime()), which is the day of the year — whenever the web server process was started and settings.py evaluated, the static content version was updated. That was dead simple and worked well enough, but it's not optimal — if you bounce your web server for maintenance between code updates, or have MaxRequestsPerChild set in your Apache config, you're going to cause some unnecessary fetches.

So I thought a little harder. I use the Mercurial source code control system. My deployment process consists of pushing the latest changes to the production repository, then at the appointed time, applying those changes and if necessary, gracefully restarting Apache. Really, the only time I need to change the version is when I push changes. So why not just use the Mercurial revision?

Enter a tiny module called revisionist, which fetches exactly that. Unzip it to a directory on your PYTHONPATH and add these lines before the earlier settings:

import revisionist
REVISION = revisionist.hgrev()

That's all there is to it. Now my static content URLs look like /57/static/css/main.css — short, sweet, and always accurate. They only change when there are real changes, proxies can't fumble them, and best of all, I never have to remember to update them.

The module also supports Bazaar and Subversion. Subversion support requires pysvn.

related files

Comments have been turned off for this article, but you can always contact us about it.