top

Cache Busting : Why You Shouldn’t Tell Your Clients To Hard Refresh

You can download the sample from GitHub @ Cache-Busting-SampleBrowser-side caching is awesome, it makes your pages load faster, reduces network usage and improves perceived load times but it starts to become a real pain when you build an application that frequently rolls out client-side updates. These updates propagate slowly to your end users with almost no way of being sure whether your bug still exists because you missed a test case or because the end user is simply still using the cached version of your JavaScript.Let’s face it, you do tell your clients to press ctrl + f5 to see the latest changes you’ve made to JavaScript.There are a few cache-busting techniques you can get around this problem, some of them beingAppend a hash to the file.Append a query string to the fileWe will be looking at the former.Gulp Rev To The Rescue!The gulp plugin gulp-rev appends content hashes to the end of file names. This is great because hashes are only computed for files that have changed. This way, the client always only downloads the assets that have changed.You need to install gulp-rev by running the commandnpm install gulp-rev — save-devHere, we have installed gulp-rev as a development dependency. It is assumed that you already have gulp installed and a basic gulpfile running. Now, we create a task that handles the job of creating file revisions based on content hashes.const gulp = require('gulp'); const rev = require('gulp-rev'); gulp.task("revision", function () {   return gulp.src(["./Scripts/dist/**/*.js", "./Scripts/dist/**/*.css"])         .pipe(rev())         .pipe(gulp.dest("./Scripts/dist"))         .pipe(rev.manifest())         .pipe(gulp.dest("./Scripts")); });In the above snippet, we select all JavaScript and CSS files and pipe it to the rev command and store the output in the folder named dist.The rev.manifest() function creates a JSON mapping our original file names to the newly created filenames with hashes. Below is what a manifest looks like.{   "assets/css/site.compiled.css": "assets/css/site-1db21d418d.compiled.css",   "js/site.min.js": "js/site-ff3eec37bc.min.js",   "vendors/vendors.js": "vendors/vendors-ebd24a3d51.js" }Serving Assets (ASP.NET)Once the files are revised we can reference them in our HTML or JS files. The problem here is that every time the hash changes you would have to manually update each reference. Quite cumbersome, so let’s make it easy.The below code samples pertain to ASP.NET and can be used similarly in the language of your choice.In the below sample we create a static method named version which takes in the path and returns the hashed file name. Here we use to leverage the revision manifest to look for the updated hash file name. We also use runtime cache to avoid having to read the manifest for every request.public static class Revision {     public static string Version(string path)     {         if (HttpRuntime.Cache[path] == null)         {             using (StreamReader sr = new StreamReader(HostingEnvironment.MapPath("~/Scripts/rev-manifest.json")))             {                 Dictionary<string, string> rev = JsonConvert.DeserializeObject<Dictionary<string, string>>(sr.ReadToEnd());                 string revedFile = rev.Where(s => path.Contains(s.Key)).Select(g => g.Value).FirstOrDefault();                 string actualPath = "/Scripts/dist/" + revedFile;                 HttpRuntime.Cache.Insert(path, actualPath);             }         }           return HttpRuntime.Cache[path] as string;     } }Now, we can use the above method in our razor view like so.<link rel=”stylesheet”href=”@MyNamespace.Revision.Version(‘~/Scripts/dist/assets/css/site.compiled.css’)”>But I have angular templatesIt gets a bit challenging with angular templates. Let’s solve that problem.Start by installing 2 more gulp packages.npm install — save-dev buffer-to- vinyl gulp-ng- configIn the gulp task revision-html below, we create a separate revision file for html named rev-manifest-html.json. This task is a dependent task for the actual gulp task ng-revision.const gulp = require('gulp'); const rev = require('gulp-rev'); const b2v = require('buffer-to-vinyl'); const ngConfig = require('gulp-ng-config'); gulp.task("revision-html", function () {   return gulp.src(["./Scripts/dist/**/*.html"])           .pipe(rev())           .pipe(gulp.dest("./Scripts/dist"))           .pipe(rev.manifest("rev-manifest-html.json"))           .pipe(gulp.dest("./Scripts")); }); gulp.task("ng-revision", ["revision-html"], function () {     var json = require('../rev-manifest-html.json');     var dummy_json = JSON.stringify({});     return b2v.stream(new Buffer(dummy_json), 'rev-manifest-html.js')         .pipe(ngConfig('my.constants', {             createModule: false,             wrap: true,             pretty: true,             constants: {                 HTML: json             }         }))         .pipe(gulp.dest("./Scripts")) });The ng-revision task reads the output of the revision-html file (the rev-manifest-html.json). This is a JSON file of html files as keys and their corresponding revisioned file names as values. We pipe this JSON into gulp angular config which then generates an angular config file named “rev-manifest- html.js”.(function () {   return angular.module("my.constants").constant("HTML", {     "views/modules/applications/applications.html": "views/modules/applications/applications.html",     "views/modules/home/home.html": "views/modules/home/home.html"   }); })();We now need to add an HTTP interceptor to angular which will intercept outgoing requests to html files and modify the request to return the correct versioned file.(function (angular) {   angular.module('my')     .factory('customHttpInterceptor', ['HTML', function (html) {         return {           request: function (config) {             if (config.url.indexOf('views/') > 0) {               if(config.url.indexOf('deployments-history/history.html') > 0){                 console.log("called history");               }               var key = config.url.replace("Scripts/dist/", "")               config.url = "Scripts/dist/" + html[key];               return config             }             return config;           }       }   }]); })(window.angular);SummaryIn this article, we saw the strategies for cache busting that can be integrated into your build pipeline. We saw how we can revision JavaScript, CSS and html files. We can follow a similar approach for other static file types like images, fonts etc. We also saw how an angular interceptor can help to transform outbound requests and how Asp.Net can be used to respond with versioned files. It is crucial to note that we at least have HTTP caching when accessing JavaScript and CSS files to reduce the load on the server. Additionally, you can think of replacing the HTTP run-time cache with Redis which can be much more effective.
Rated 4.0/5 based on 40 customer reviews
Normal Mode Dark Mode

Cache Busting : Why You Shouldn’t Tell Your Clients To Hard Refresh

Tanmay dharmaraj
Blog
31st May, 2018
Cache Busting : Why You Shouldn’t Tell Your Clients To Hard Refresh

You can download the sample from GitHub @ Cache-Busting-Sample

Browser-side caching is awesome, it makes your pages load faster, reduces network usage and improves perceived load times but it starts to become a real pain when you build an application that frequently rolls out client-side updates. These updates propagate slowly to your end users with almost no way of being sure whether your bug still exists because you missed a test case or because the end user is simply still using the cached version of your JavaScript.

Let’s face it, you do tell your clients to press ctrl + f5 to see the latest changes you’ve made to JavaScript.

There are a few cache-busting techniques you can get around this problem, some of them being

  • Append a hash to the file.
  • Append a query string to the file

We will be looking at the former.


Gulp Rev To The Rescue!

The gulp plugin gulp-rev appends content hashes to the end of file names. This is great because hashes are only computed for files that have changed. This way, the client always only downloads the assets that have changed.

You need to install gulp-rev by running the command

npm install gulp-rev — save-dev

Here, we have installed gulp-rev as a development dependency. It is assumed that you already have gulp installed and a basic gulpfile running. Now, we create a task that handles the job of creating file revisions based on content hashes.

const gulp = require('gulp');
const rev = require('gulp-rev');
gulp.task("revision", function () {
  return gulp.src(["./Scripts/dist/**/*.js", "./Scripts/dist/**/*.css"])
        .pipe(rev())
        .pipe(gulp.dest("./Scripts/dist"))
        .pipe(rev.manifest())
        .pipe(gulp.dest("./Scripts"));
});


In the above snippet, we select all JavaScript and CSS files and pipe it to the rev command and store the output in the folder named dist.

The rev.manifest() function creates a JSON mapping our original file names to the newly created filenames with hashes. Below is what a manifest looks like.

{
  "assets/css/site.compiled.css": "assets/css/site-1db21d418d.compiled.css",
  "js/site.min.js": "js/site-ff3eec37bc.min.js",
  "vendors/vendors.js": "vendors/vendors-ebd24a3d51.js"
}


Serving Assets (ASP.NET)

Once the files are revised we can reference them in our HTML or JS files. The problem here is that every time the hash changes you would have to manually update each reference. Quite cumbersome, so let’s make it easy.

The below code samples pertain to ASP.NET and can be used similarly in the language of your choice.

In the below sample we create a static method named version which takes in the path and returns the hashed file name. Here we use to leverage the revision manifest to look for the updated hash file name. We also use runtime cache to avoid having to read the manifest for every request.

public static class Revision
{
    public static string Version(string path)
    {
        if (HttpRuntime.Cache[path] == null)
        {
            using (StreamReader sr = new StreamReader(HostingEnvironment.MapPath("~/Scripts/rev-manifest.json")))
            {
                Dictionary<string, string> rev = JsonConvert.DeserializeObject<Dictionary<string, string>>(sr.ReadToEnd());
                string revedFile = rev.Where(s => path.Contains(s.Key)).Select(g => g.Value).FirstOrDefault();
                string actualPath = "/Scripts/dist/" + revedFile;
                HttpRuntime.Cache.Insert(path, actualPath);
            }
        }
 
        return HttpRuntime.Cache[path] as string;
    }
}


Now, we can use the above method in our razor view like so.

<link rel=”stylesheet”
href=”@MyNamespace.Revision.Version(‘~/Scripts/dist/assets/css/site.compiled.css’)”>


But I have angular templates

It gets a bit challenging with angular templates. Let’s solve that problem.
Start by installing 2 more gulp packages.

npm install — save-dev buffer-to- vinyl gulp-ng- config

In the gulp task revision-html below, we create a separate revision file for html named rev-manifest-html.json. This task is a dependent task for the actual gulp task ng-revision.

const gulp = require('gulp');
const rev = require('gulp-rev');
const b2v = require('buffer-to-vinyl');
const ngConfig = require('gulp-ng-config');

gulp.task("revision-html", function () {
  return gulp.src(["./Scripts/dist/**/*.html"])
          .pipe(rev())
          .pipe(gulp.dest("./Scripts/dist"))
          .pipe(rev.manifest("rev-manifest-html.json"))
          .pipe(gulp.dest("./Scripts"));
});

gulp.task("ng-revision", ["revision-html"], function () {
    var json = require('../rev-manifest-html.json');
    var dummy_json = JSON.stringify({});
    return b2v.stream(new Buffer(dummy_json), 'rev-manifest-html.js')
        .pipe(ngConfig('my.constants', {
            createModule: false,
            wrap: true,
            pretty: true,
            constants: {
                HTML: json
            }
        }))
        .pipe(gulp.dest("./Scripts"))
});


The ng-revision task reads the output of the revision-html file (the rev-manifest-html.json). This is a JSON file of html files as keys and their corresponding revisioned file names as values. We pipe this JSON into gulp angular config which then generates an angular config file named “rev-manifest- html.js”.

(function () {
  return angular.module("my.constants").constant("HTML", {
    "views/modules/applications/applications.html": "views/modules/applications/applications.html",
    "views/modules/home/home.html": "views/modules/home/home.html"
  });
})();


We now need to add an HTTP interceptor to angular which will intercept outgoing requests to html files and modify the request to return the correct versioned file.

(function (angular) {
  angular.module('my')
    .factory('customHttpInterceptor', ['HTML', function (html) {
        return {
          request: function (config) {
            if (config.url.indexOf('views/') > 0) {
              if(config.url.indexOf('deployments-history/history.html') > 0){
                console.log("called history");
              }
              var key = config.url.replace("Scripts/dist/", "")
              config.url = "Scripts/dist/" + html[key];
              return config
            }
            return config;
          }
      }
  }]);
})(window.angular);


Summary

In this article, we saw the strategies for cache busting that can be integrated into your build pipeline. We saw how we can revision JavaScript, CSS and html files. We can follow a similar approach for other static file types like images, fonts etc. We also saw how an angular interceptor can help to transform outbound requests and how Asp.Net can be used to respond with versioned files. It is crucial to note that we at least have HTTP caching when accessing JavaScript and CSS files to reduce the load on the server. Additionally, you can think of replacing the HTTP run-time cache with Redis which can be much more effective.

Tanmay

Tanmay dharmaraj

Blog author

Tanmay loves building web applications and has been working with Office365 and Azure for 5+ years. He currently works at Rapid Circle as a technical consultant.

Leave a Reply

Your email address will not be published. Required fields are marked *

SUBSCRIBE OUR BLOG

Follow Us On

Share on

other Blogs

20% Discount