By david on August 03, 2010

Image caching at the browser level is one of the most efficient way to boost web site performance.

In some of our web application, we have to render dynamic images stored in Azure Blob Storage. Without image caching at the browser level, rendering these images at the browser will be a huge performance hit, in particular when we have 20-30 images to render in a given page.

Here are some of the techniques we have used in the past:

  1. Sent an ETag value in the HTTP response to avoid unnescessary network bandwidth.
  2. Sent an Expires value in the HTTP response to avoid server calls within short period of time.

The result of these two technique yielded a much better user experience while still maintaining the ability to render dynamic images in the browser.

Here are the rundown:

1. Sent an ETag value in the HTTP response
By sending a unique ETag value in the (original) HTTP response (it can be any format of a string), the next time the browser was going to fetch the same image (any URI - as a matter of fact), it would include the "If-None-Match" header with the ETag value in the request. So, at the server side, we could simply check the ETag value in the response as well as the current ETag value of the image (or any resources in general), if they were the same, then the server would simply send HTTP Response 304 - Not Modified, instead of the image. The browser would then use the image in its cache instead. This was a lot less Network Bandwidth.

Below would be what the HTTP Response and Request look like:

Original Response: HTTP/1.1 200 OK Cache-Control: private Content-Type: image/jpeg [... deleted] Etag: 5a3b14bfb9dc15dd0823b3a647681d50 [... deleted] Date: Tue, 03 Aug 2010 14:24:08 GMT Content-Length: 1777

Subsequent Request (as long as the browser still retain its cache):

GET [uri address] HTTP/1.1 Host: [host address] User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 [... deleted] If-None-Match: 5a3b14bfb9dc15dd0823b3a647681d50

The Response to this subsequent Request:

HTTP/1.1 304 Not Modified Cache-Control: no-cache Pragma: no-cache Content-Type: text/html [... deleted] Date: Tue, 03 Aug 2010 14:26:01 GMT

That's all it on the HTTP Response. Nothing on the body. Very efficient network bandwidth utilization.

2. Sent an Expires value in the HTTP response
Eventhough the above technique would make the web page loads faster, it would still make server calls, and if we had 20-30 images on the page, that would still add up. So, the next thing we could do to improve user's experience, at least within the duration of the visit to the web site, we also sent HTTP Header "Expires" in the original response. What this did was to let the browser knew, that the next time it neededs the same image (any URI in general) - it would check the datetime stamp on this Expires field and if the image/resource did not yet expire, it would not try to fetch it from the server. We did this when it was not critical for the user to get real-time dynamic resource update. E.g. if someone updated the image, it was OK for the rest of the users to see the new image within the next half hour or so. BTW, user could always hit Ctrl+F5 (in most browsers) to force a refresh.

Below would be what the HTTP Response look like:

HTTP/1.1 200 OK Cache-Control: private Content-Type: image/jpeg Expires: Tue, 03 Aug 2010 15:04:03 GMT [... deleted] Date: Tue, 03 Aug 2010 14:34:03 GMT Content-Length: 93870

So, after the first time image(s) were fetched (or get the 304 Not Modified Response), within the next 30 minutes, as user browsed the web site, any request for the same image(s) would simply be retrieved from the local browser cache, no trip to the server whatsoever.

Much better user experience.