Managing Static Files & Browser Caching

posted on 01/14/09 at 12:26:48 am by Joel Ross

As a web developer, if you have a static file, and it gets updated, how do you ensure that it gets downloaded by your users, instead of using the previously cached version?

That's the problem I was faced with the other day. I had a JavaScript file that I was updating, and trying to figure out the best way to ensure it got refreshed on the client-side. We'd been using a couple of different methods, but they required manual steps to get them working:

  1. Rename the file. If I had a file named app.js, I could rename that to app_001.js. Then app_002.js, etc.
  2. Add a query string to the file. Reference the file in the page as app.js?version=001. Then app.js?version=002.

Both of the above solutions work. But they have issues. They're both manual, and have to be 1.) remembered each time the file is changed, and 2.) well known by every team member.

The other downside is that every place that file is referenced in the code needs to be updated. For most files like that, they're referenced in one place, but sometimes (especially as the project grows) things get out of hand, and it will get referenced in multiple places. So, after talking through the problem, I think we came up with a nice solution. It accomplishes two goals - it centralizes the references of the files (without being too invasive on existing code), and automates the process of telling the browser to grab a new version when the file is updated.

So how does it work? Let's look at some code. I created a new class called JavascriptUrlBuilder to do the work for me. It takes in a string, checks the timestamp of the file, and tacks that onto the querystring. Then it caches that information so subsequent requests don't have to hit the file system every time. Here's the actual code:

   1:  public class JavascriptUrlBuilder
   2:  {
   3:     private static object _syncObject = new object();
   5:     public static string GetUrlFor(string javascriptFile)
   6:     {
   7:        if(HttpContext.Current.Cache[javascriptFile] == null)
   8:        {
   9:           lock(_syncObject)
  10:           {
  11:              if(HttpContext.Current.Cache[javascriptFile] == null)
  12:              {
  13:                 var fileInfo = new FileInfo(HttpContext.Current.Server.MapPath(javascriptFile));
  14:                 var filePath = String.Format({0}?v={1}{2}{3}{4}{5}{6}",
  15:                                       VirtualPathUtility.ToAbsolute(javascriptFile),
  16:                                                   fileInfo.LastWriteTimeUtc.Year.ToString("0000"),
  17:                                                   fileInfo.LastWriteTimeUtc.Month.ToString("00"),
  18:                                                   fileInfo.LastWriteTimeUtc.Day.ToString("00"),
  19:                                                   fileInfo.LastWriteTimeUtc.Hour.ToString("00"),
  20:                                                   fileInfo.LastWriteTimeUtc.Minute.ToString("00"),
  21:                                                   fileInfo.LastWriteTimeUtc.Second.ToString("00"));
  22:                 HttpContext.Current.Cache.Insert(javascriptFile, 
  23:                                          filePath, 
  24:                                          new CacheDependency(fileInfo.FullName));
  25:              }
  26:           }
  27:        }
  29:        return HttpContext.Current.Cache[javascriptFile].ToString();
  30:     }
  31:  }

The nice part is that it uses a cache dependency on the file system, so anytime the file is updated, the cache gets evicted, and you get a new timestamp, thus forcing browsers to re-download the file.

To use it, it's as simple as this:

   1:  this.ClientScript
   2:      . RegisterClientScriptInclude(this.GetType(), 
   3:                             "jquery", 
   4:                             JavascriptUrlBuilder.GetUrlFor(JavascriptFiles.JQuery));

You'll also notice that I've created a class with a set of constants that define all of the different JavaScript files we have, making it easy to know what's available, and maintaining all of them in one place.

By doing this, we've accomplished both goals we set out to do: browsers will always have the latest files without any manual steps for the developers, and it centralizes how the files are referenced. Yes, you can still add them to a page where ever you want, but if I ever need to change the name of a file (for example, from jquery-1.2.6.js to the next rev), I just change the one reference, and all of them get updated automatically.

I'm sure there's a better way to handle this, but this works and is relatively automated. What have you done to handle this issue?

Categories: Development, C#