sketchboard blog

How to prevent CDN caching your updated JavaScript files

It is very annoying when you have updated your JavaScript files and then browser or some other caching entity caches your files and your latest changes are not there. I have tried to prevent this happening by hashing JavaScript file names manually, but process was very error prone. Therefore process was automated with small helper called Minish.

When you change JavaScript files to be loaded from CDN to provide faster application load time. It is necessary to use reliable way to update hash names for JavaScript files or CND could cache those for a long time.

To make sure that updated JavaScript files are correctly loaded, Minish minifies (using UglifyJS2) and then hashes JavaScript file names based on file content. File name is updated only if content is changed. Minish contains also snippets to discover latest hashed files using Go or Scala to make sure most recent file is used.

With hashed file names it is safe to let any caching entity to cache your JavaScript files. You can even use very long Cache-Control expiration times.

Hashed file loaded from the webserver

Example:

<script src="82de87aebb4e787093202ddc68f74264-app.min.js"></script>

Configuration

Makefile

Outputs hashed minified JavaScript file to dist directory.

minify:
	minish static/js/app/app.js dist

Web Application Configuration (Go)

Find latest hashed file based on modified time on your application boot.

appJs, err := util.FindLatest("../dist"), regexp.MustCompile(`^\S+-app\.min\.js$`))

Provide correct hashed file name for the html template. On production .AppJs contains hashed minified file name and during development .AppJs contains unminified app.js file.

<script type="text/javascript" src="{\{ .AppJs }}"></script>

Web application Configuration (Scala/Liftweb)

In Liftweb you can write your own snippet to discover latest hashed file.

<script data-lift='ProductionOnlyWithHashCdnPrefix' data-attr='src' src='/dist/presentation.min.js'></script>

Sample Snippet implementation

object ProductionOnlyWithHashCdnPrefix {
  def render(in: NodeSeq): NodeSeq = if (!Props.devMode) {
    ({
      val attr = (in \ "@data-attr").map(attr => attr.text).mkString("")
      val attrval = (in \ ("@" + attr)).map(v => v.text).mkString("")

      val hashedName = cachedNames.get(attrval) match {
        case Some(n) => staticPrefix + n
        case _ => {
        	findLatest("../dist", attrval) match {
        		case some(hn) => {
        			cacheNames =+ attrval -> hn
        			hn
        		}
        		case failure => {
        			// handle error
        		}
        	}
        }
      }

      ("* [" + attr + "]") #> hashedName
    }).apply(in)
  } else {
    NodeSeq.Empty
  }
}

Nginx Configurations

location ~* ^/static/.+\.(jpg|jpeg|png|css|js|html|gif)$ {
  access_log off;
  expires 30d;
  root /usr/webapp/dist;
}

expires 30d configures nginx to respond with correct Cache-Control header value to cache your files in browser or in CDN.

CloudFront Configuration

Above Nginx configuration lets Nginx to control your Cache-Control expiration time and how long CloudFront should keep files in its own cache.