Versioning Static Assets With Grunt

Who am I ?

Alexandrine Boissière

Senior Software Engineer at HootSuite

Twitter : @theasta

Static assets ?

Why do we need to version static assets ?

Rule #1: Make fewer http requests

Rule #3: Add an expires header

HTTP Validation Model

HTTP Headers

  • Last-Modified / If-Modified-Since
  • Etag / If-None-Match

HTTP Expiration Model

HTTP Headers

  • Cache-Control: max-age=2592000
  • Expires:Sat, 22 Jun 2013 05:00:00 GMT

No request to the server for the next month !!

When I say no request, I mean it !

Cache-Busting Techniques

  • /js/base.js?v=1.2.0 (BAD)
  • /1.2.0/js/base.js
  • /js/base.1.2.0.js

Old HootSuite

/12345/js/base.js

/12345/css/homepage.css

/12345/images/bg.png

12345 folder = release tag

Versioning By Release Number

Continuous deployment + per-release assets versioning = bad match !!

Version Static Assets on a per-file basis

Problems to solve

  • Deployment:
    • Automate renaming of assets files
    • Process and upload only when necessary
  • Codebase:
    • Retrieve the versioned filenames

SPOILER !!!

Deployment Time for static assets:

Old HootSuite: 11 min

New HootSuite: 1 min

Deployment

Grunt

A JavaScript task runner built on top of Node.js

Gruntfile.js


module.exports = function(grunt) {
  grunt.initConfig({
    uglify: { ... },
    cssmin: { ... },
    s3: { ... }
  });

  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-cssmin');
  grunt.loadNpmTasks('grunt-s3');
  grunt.registerTask('deploy', ['uglify', 'cssmin', 's3']);
};
grunt <taskName>

Task Configuration - Files

Step 1: Write a deployment script - js


grunt.initConfig({
  uglify: {
    home: {
      files: {
        'build/js/home.js': ['js/home.js', 'js/home_utils.js']
      }
    },
    contact: {
      files: {
        'build/js/contact.js': ['js/contact_form.js']
      }
    }
  }
});
grunt uglify
grunt uglify:home

Step 1: Write a deployment script - css


grunt.initConfig({
  cssmin: {
    main: {
      files: {
        'build/css/main.css': ['css/layout.css', 'css/buttons.css']
      }
    }
  }
});
grunt cssmin

Register a deployment task


grunt.registerTask('deploy', ['uglify', 'cssmin', 's3']);
grunt deploy

SPOILER !!!

Old HootSuite: 11 min

Now: 4 min

Step 2 : Version files

The renaming logic is very simple

  • Revving with a date
    • Use the last modification time
  • Revving with a md5 hash
    • Hash the file content

Declarative Syntax !!

Solution: An assets-versioning plugin

Example


assets_versioning: {
    home: {
      options: {
        multitask: 'uglify'
      }
    }
  }

options.skipExisting

Won't run the cloned task if ever the versioned destination file already exists.

and plenty other options...

Codebase

How to retrieve the versioned filename ?

/images/loader.gif -> /images/loader.20130413004500.gif

options.output: generates a json file with both original and versioned filenames.

But .. css ?

Solution : Use a css preprocessor (less, sass)


// images.less
@loader_gif: 'loader.gif';
@buttons_btn-cta_png: 'buttons/btn-cta.png';

.btn-cta {
    color: #384602;
    background: #accd3d url("@{base-image}@{buttons_btn-cta_png}") 0 center repeat-x;
}

Where to get that plugin ?

NPM

npm install grunt-assets-versioning

Github

https://github.com/theasta/grunt-assets-versioning

THE END

By Alexandrine Boissière - @theasta

alexandrine.boissiere@hootsuite.com