One way to improve performance and instantly speed up the load times of your site is to start caching resources. If your server is sending the correct headers, then the browser will automatically cache (save) certain resources so that the next time the user visits your site, the resources are already present and do not need to be re-downloaded.
But what happens when the file on the server actually has been updated? How do you notify the browser to download the new file to replace the one in the cache?
That’s where cache busting comes in. There’s a great article covering a number of different cache busting strategies over on CSS-Tricks – in fact, it even mentions using a task runner like Gulp or Grunt to get the job done – just like we’ll be doing. However, the solutions and plugins presented in that article are, for the most part, specific to a particular language or implementation.
Below we’ll be taking a look at a simple cache busting method that also uses Gulp, but is language agnostic and will work in any environment.
Installing Dependencies
First things first, we need to install a couple of dependencies using npm. We’re going to be using Gulp as our task runner along with the gulp-replace, gulp-rename, del and run-sequence plugins in order to get our cache busting to work. The names already kind of give away what these plugins do, but we’ll explain each one as needed later on. To install these dependencies all in one go open up your terminal and run the following command:
$ npm install --save-dev gulp gulp-replace gulp-rename del run-sequence
p.s. don’t forget the –save-dev flag to save these dependencies to your package.json file.
Application Structure
Now that we’ve installed our dependencies, image the following application structure:
- src
- css
- style.css
- js
- app.js
- templates
- index.template.html
- css
- dist
- index.html
- gulpfile.js
In the root of our project we’ve got the entry page to our app – index.html – along with our gulpfile which contains the configuration for Gulp. We also have two sub-directories – src contains our custom css, js and our template files. In our case the template files are written in basic html, but they could be PHP files, JSPs or any other file type or language. Finally the dist folder will contain the processed assets after they’ve been passed through our cache busting technique.
Your app could be structured completely differently, but the example above will help us visualize the changes happening to the files in the next section.
Setting Up Our Gulpfile
Now it’s time to actually work on setting up our cache busting resources. My solution is actually the simplest thing you can imagine – generate a timestamp, append the timestamp to the resource file names and update the references to the resources with the new filename. It doesn’t need to be a timestamp per se, it could be anything that acts as a unique identifier – a semantic version number for example.
All the code for this will be inside our gulp configuration file. So we need to open up gulpfile.js and start off by importing our dependencies, declaring our timestamp variable and setting up a default Gulp task.
var gulp = require('gulp'); var rename = require("gulp-rename"); var replace = require("gulp-replace"); var del = require('del'); var sequence = require('run-sequence'); var timestamp; gulp.task('default', []);
The timestamp variable needs to be declared as a global so that it can be used by the different tasks we’re about to add. The default task is run when you type gulp in the terminal in the project folder. So if you run Gulp now you should get the below output in your terminal:
[19:10:54] Starting 'default'... [19:10:54] Finished 'default' after 3.76 μs
Once you get that working we need to setup the rest of our Gulp tasks which actually do all the work. We’ll need a different task for updating the timestamp, processing the CSS files, processing the JS files and processing the template files.
gulp.task('build-css', function () { return gulp.src('src/css/*.css') .pipe(rename(function (path) { path.basename += "-" + timestamp; })) .pipe(gulp.dest('dist/')); }); gulp.task('build-js', function() { return gulp.src('src/js/*.js') .pipe(rename(function (path) { path.basename += "-" + timestamp; })) .pipe(gulp.dest('dist/')); }); gulp.task('build-templates', function () { return gulp.src('src/templates/*.template.html') .pipe(rename(function (path) { path.basename = path.basename.replace('.template', ''); //remove the template part from the filename })) .pipe(replace('<!TIMESTAMP!>', '-' + timestamp)) .pipe(gulp.dest('.')); //output in the root directory }); gulp.task('gen-timestamp', function() { timestamp = new Date().getTime(); });
build-js and build-css use the gulp-rename plugin to just append the timestamp to the end of the file name. build-templates uses gulp-replace to search for <!TIMESTAMP!> and replace it with the generated timestamp value. So now we also need to update the references to our resources in our template file to actually include the <!TIMESTAMP!> keyword.
Currently in the head of our index.template.html we reference our CSS and JS files like this.
<head> <link rel="stylesheet" href="/src/css/style.css"> <script src="/src/js/app.js"></script> </head>
We need to add <!TIMESTAMP!> to wherever we want the generated value to appear. Plus, we also need to change the references to now point to the dist folder.
<head> <link rel="stylesheet" href="/dist/style-<!TIMESTAMP!>.css"> <script src="/dist/app.js-<!TIMESTAMP!>"></script> </head>
It’s important to pick a placeholder string that will not be added anywhere else by mistake. You can use any symbols that you want in the replacement string, since the browser will never see it, but will only see the generated version number.
Now to make sure this works as planned, the tasks needs to be called in the correct order. Otherwise the timestamp var might not have been initialised before the other tasks start running. One of the “problems” with Gulp is that task dependencies run asynchronously. This is great for performance but can be a bit of a headache to work around if you just want certain tasks to run to completion before other tasks start.
The easiest way to get this to work for me was to use the run-sequence plugin that we installed earlier. It will allow us to specify which tasks to run sequentially and which ones to run asynchronously (so we don’t completely lose the performance gains of running tasks in parallel). So instead of setting up our default task the regular way:
gulp.task('default', ['gen-time', 'build-js', 'build-css', 'build-templates']);
We can use the run-sequence for some more fine-grained control:
gulp.task('default', function () { sequence('gen-time', ['build-js', 'build-css', 'build-templates']); });
Any tasks inside an array will run in parallel and will wait for previous tasks to finish. Run Gulp again and you should see gen-time starting and stopping before the rest of the tasks start to execute.
[20:18:25] Starting 'default'... [20:18:25] Starting 'gen-time'... [20:18:25] Finished 'gen-time' after 107 μs [20:18:25] Finished 'default' after 6.94 ms [20:18:25] Starting 'build-js'... [20:18:25] Starting 'build-css'... [20:18:25] Starting 'build-templates'... [20:18:25] Finished 'build-templates' after 63 ms [20:18:25] Finished 'build-js' after 88 ms [20:18:26] Finished 'build-css' after 229 ms
So far so good. But if you keep running Gulp you might notice that the old files are not automatically overwritten with the new ones since their names are different each time resources are built. So let’s add another Gulp task which uses the del plugin to delete the contents of the dist folder before processing our files.
gulp.task('delete', function () { return del([ 'dist/' ]); }); gulp.task('default', function () { sequence(['delete', 'gen-time'], ['build-js', 'build-css', 'build-templates']); });
Run Gulp again and your resources should now be completely setup to bust some cache!
And just as a final touch, let’s watch for changes so the resources are only updated if the code has been modified. We can also remove run-sequence from the default task, since we now just call watch as a dependency.
gulp.task('watch', function() { gulp.watch('src/js/*.js', function () { sequence(['delete', 'gen-time'], ['build-js', 'build-css', 'build-templates']); }); gulp.watch('src/css/*.css', function () { sequence(['delete', 'gen-time'], ['build-js', 'build-css', 'build-templates']); }); }); gulp.task('default', ['watch']);
Make It Better
Even though the above solution gets the job done, there’s still a little room for improvement. You might have noticed that if you run Gulp both the JS and the CSS resources get updated even if you only modify one. So even if you only changed the CSS file, the JS file will also need to be re-downloaded by the browser.
You can get around this by introducing separate delete and build-templates tasks for JS and CSS. This could get very cumbersome if there are multiple files and you want each one to have its own cache busting versioning. But until HTTP2 is fully adopted you’ll probably still be concatenating resources into a single file which can be piped through the above process.
So there you have it – my straightforward [and verbose] cache busting solution. Feel free to let me know in the comments if you have a better way of cache busting your resources!
Leave a Reply