Leverage browser caching in IIS (google pagespeed issue)

Most of the browser caching issues can be resolved by viewing the response headers (can be done in Google chrome developer tools).

enter image description here

Now the clientCache section of your web.config file should set your output caching to a maximum age as you see in the image below has set the max-age to 86400 which is 1 day in seconds.

Here is the web.config snippet for this setup.

<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />

Now thats great, the response header has a max-age property set on the Cache-Control header. So the browser should be caching the content. Well this is mostly true but some browsers require another flag to be set. Specifically the public flag set for the cache control header. This can be easily added by using the cacheControlCustom attribute in the web.config. Here is an example.

<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />

Now when we retry the page and inspect the headers.

enter image description here

Now as you can see from the image above we now have the value public, max-age=86400. So our browser has all it needs to cache the resources. Now examining the headers and the network tab of google chrome will help us.

Here is the first request to the file.. Note the file is not cached…
enter image description here

Now lets navigate back to this page (NOTE: do not refresh the page, we will talk about that in a second). You will see the response in now returning from cache (as circled).

enter image description here

Now what happens if I refresh the page using either F5 or using the browser refresh feature. Wait.. where did the (from cache) go.
enter image description here

Well in Google Chrome (not sure about other browsers) using the refresh button will re-download the static resources regardless of the cache header (insert clarification here please). That means that the resources has been re-retrieved and the max age header sent over.

Now after all the explanation above, be sure to test how you are monitoring the cache headers.

Update

Based on your comments you stated you have a generic handler (IHttpHandler) named Image.ashx with the content type of image/jpg. Now you may expect the default behaviour would be to cache this handler. However IIS sees the extension .ashx (correctly) as a dynamic script and is not subject to caching without explicitly setting the cache headers in the code itself.

Now this is where you need to be careful, as typically IHttpHandlers should infact not be cached as they usually deliver dynamic content. Now if that content is unlikely to change you could set your cache headers directly in the code. Here is an example of setting cache headers in IHttpHandlers using the Response context.

context.Response.ContentType = "image/jpg";

context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetSlidingExpiration(true);

context.Response.TransmitFile(context.Server.MapPath("~/out.jpg"));

Now looking at the code we are setting a few properties on the Cache property. To get the desired response I have set the properties.

  • context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1)); tells the out put cache to set the max-age= part of the Cache-Control header to be 1 day in the future (86400 seconds).
  • context.Response.Cache.SetCacheability(HttpCacheability.Public); tells the out put cache to set the Cache-Control header to public. This is quite important as it tells the browser to cache to object.
  • context.Response.Cache.SetSlidingExpiration(true); tells the output cache to ensure it is setting the max-age= part of the Cache-Control header properly. Without setting the sliding expiration, the IIS out put caching will ignore the max age header. Putting this together gives me this result.

output cache from ashx file

As I stated above you may not want to cache the .ashx files as they typically deliver dynamic content. However if that dynamic content is not likely to change in a given period you can use the methods above to deliver your .ashx file.

Now in conjunction with the process listed above you could also set the ETag (see wiki) component of the cache headers so the browser can verify the content being delivered by a custom string. The wiki states:

An ETag is an opaque identifier assigned by a web server to a specific
version of a resource found at a URL. If the resource content at that
URL ever changes, a new and different ETag is assigned.

So this is really some sort of unique identification for the browser to identify the content being delivered in the response. By providing this header the browser on the next reload will send over a If-None-Match header with the ETag from the last response. We can modify our handler to detect the If-None-Match header and compare it to our own generated Etag. Now there is no exact science to generating ETags but a good rule of thumb is to deliver an identifier that will most likely define only one entity. In this case I like to use two strings concatenated together such as.

System.IO.FileInfo file = new System.IO.FileInfo(context.Server.MapPath("~/saveNew.png"));
string eTag = file.Name.GetHashCode().ToString() + file.LastWriteTimeUtc.Ticks.GetHashCode().ToString();

In the snippet above we are loading a file from our file system (you could get this from anywhere). I am then using the GetHashCode() method (on all objects) to get the integer hash code of the object. In the example I concat the hash of the file name, then the last write date. The reason for the last write date is in case the file is changed the hash code is changed as well, thus making the finger prints different.

This will generate a hash code similar to 306894467-210133036.

So how do we use this in our handler. Below is the newly modified version of the handler.

System.IO.FileInfo file = new System.IO.FileInfo(context.Server.MapPath("~/out.png"));
string eTag = file.Name.GetHashCode().ToString() + file.LastWriteTimeUtc.Ticks.GetHashCode().ToString();
var browserETag = context.Request.Headers["If-None-Match"];

context.Response.ClearHeaders();
if(browserETag == eTag)
{
    context.Response.Status = "304 Not Modified";
    context.Response.End();
    return;
}
context.Response.ContentType = "image/jpg";
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetSlidingExpiration(true);
context.Response.Cache.SetETag(eTag);
context.Response.TransmitFile(file.FullName);

As you can see, I have changed quite alot of the handler however you will notice that we generate the Etag hash, check for an incoming If-None-Match header. If the etag hash and the header are equal then we tell the browser that the content hasnt changed by returning the status code 304 Not Modified.

Next was the same handler except we add the ETag header by calling:

context.Response.Cache.SetETag(eTag);

When we run this up in the browser we get.

Cache-Control with ETag

You will see from the image (as i did change the file name) that we now have all the components of our cache system in place. The ETag is being delivered as a header, and the browser is sending the request header If-None-Match so our handler can respond accordingly to cache file changed.

Leave a Comment