April 17th, 2011

Safe Javascript/Asset caching with Node.js and nginx

Most web applications will have a large chunk of Javascript that needs to be available every pageload, one of my applications clocks in at 134k (minified, not gzipped).

We can easily tell the browser to cache this by setting Expires headers, however, this leads to problems when you roll out a new version: the user has to refresh the page (not simply load it) to get the new version. If you have some inline Javascript, and it gets out of sync with the user's cached version of your static Javascript, then you have problems.

Fortunetly this problem has been solved elsewhere, and all we have to do is adapt it to Node. Rails does this by appending `?file_mtime` to the url of all assets. When a new version of an asset is deployed, the file modification time changes, thus changing the URL, which in turn causes the browser to request the new version.

Configuring nginx to set the expires header for the url?timestamp style is discussed in a great article here.

Now that nginx is setting a maximum Expires header as appropriate, all we have to do is append the timestamp to all of our URLs:

var findit = require('findit');
var fs = require('fs');

function asset_hrefs(dir) {
  var asset_hrefs = {};
  var files = findit.findSync(dir);
  files.forEach(function(file, i) {
    var href = file.substring(dir.length);
    asset_hrefs[href] = [href, '?', fs.statSync(file).mtime.getTime()].join('');
  });
  return asset_hrefs;
};

// Run this once, when your application starts
var hrefs = asset_hrefs('public');

Pass in the directory where all your static assets live (in my case 'public'), and that function will return an Object that looks like this:

{
  '/js/all.js': '/js/all.js?1303065298000',
   ...
  '/css/style.css': '/css/style.css?1303065297456'
}

So, all you have to do is pass this structure to your view renderer, and use it like so (here I'm using Jade, and I've called the structure hrefs):

script(src=hrefs['/js/all.js'], type="text/javascript")

hrefs['/js/index.js'] of course maps to '/js/all.js?1303065298000'. Now the actual url for your Javascript file will change whenever that file is modified, so you never run the risk of the using running old code.