One easy way to enhance the experience of users visiting your Web site by increasing the perceived performance of navigating in your site is to reduce the number of HTTP requests that are required to display a page. There are several techniques for achieving this, such as merging scripts into a single file, merging images into a big image, etc, but by far the simplest one of all is making sure that you cache as much as you can in the client. This will not only increase the rendering time but will also reduce load in your server and will reduce your bandwidth consumption.
Unfortunately the different types of caches and the different ways of set it can be quite confusing and esoteric. So my recommendation is to think about one way and use that all the time, and that way is using the HTTP 1.1 Cache-Control header.
So first of all, how do I know if my application is being well behaved and sending the right headers so browsers can cache them. You can use a network monitor or tools like Fiddler or wfetch to look at all the headers and figure out if the headers are getting sent correctly. However, you will soon realize that this process won't scale for a site with hundreds if not thousands of scripts, styles and images.
Enter Site Analysis - IIS Search Optimization Toolkit
To figure out if your images are sending the right headers you can follow the next steps:
- Install the IIS Search Optimization Toolkit from http://www.iis.net/extensions/SEOToolkit
- Launch InetMgr.exe (IIS Manager) and crawl your Web Site. For more details on how to do that refer to the article "Using Site Analysis to crawl a web site".
- Once you are in the Site Analysis dashboard view you can start a New Query by using the Menu "Query->New Query" and add the following criteria:
- Is External - Equals - False -> To only include the files that are coming from your Web site.
- Status code - Equals - OK -> To include only successful requests
- Content Type Normalized - Begines With - image/ -> To include only images
- Headers - Not Contains - Cache-Control: -> to include the ones does not have the cache-control header specified
- Headers - Not Contains - Expires: -> To include only the ones that do no have the expires header
- Press Execute, and this will display all the images in your Web site that are not specifying any caching behavior.
Alternatively you can just save the following query as "ImagesNotCached.xml" and use the Menu "Query->Open Query" for it. This should make it easy to open the query for different Web sites or keep testing the results when making changes:
<expression field="IsExternal" operator="Equals" value="False" />
<expression field="StatusCode" operator="Equals" value="OK" />
<expression field="ContentTypeNormalized" operator="Begins" value="image/" />
<expression field="Headers" operator="NotContains" value="Cache-Control:" />
<expression field="Headers" operator="NotContains" value="Expires:" />
<field name="URL" />
<field name="ContentTypeNormalized" />
<field name="StatusCode" />
How do I fix it?
In IIS 7 this is trivial to fix, you can just drop a web.config file in the same directory where your images and scripts and CSS styles specifying the caching behavior for them. The following web.config will send the Cache-Control header so that the browser caches the responses for up to 7 days.
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
Furthermore, using the same query above in the Query Builder you can Group by Directory and find the directories that really worth adding this. For that is just matter of clicking the "Group by" button and adding the URL-Directory to the Group by clauses. Not surprisingly in my case it flags the App_Themes directory where I store 8 images.
Finally, what about 304's?
One thing to note is that that even if you do not do anything most modern browsers will use conditional requests to reduce the latency if they have a copy in their cache, as an example, imagine the browser needs to display logo.gif as part of displaying test.htm and that image is available in their cache, the browser will issue a request like this
GET /logo.gif HTTP/1.1 Accept: */* Referer: http://carlosag-client/test.htm Accept-Language: en-us User-Agent: (whatever-browser-you-are-using) Accept-Encoding: gzip, deflate If-Modified-Since: Mon, 09 Jun 2008 16:58:00 GMT If-None-Match: "01c13f951cac81:0" Host: carlosagdev:8080 Connection: Keep-Alive
Note the use of If-Modfied-Since header which tells the server to only send the actual data if it has been changed after that time. In this case it hasn't so the server responds with a status code 304 (Not Modified)
HTTP/1.1 304 Not Modified Last-Modified: Mon, 09 Jun 2008 16:58:00 GMT Accept-Ranges: bytes ETag: "01c13f951cac81:0" Server: Microsoft-IIS/7.0 X-Powered-By: ASP.NET Date: Sun, 07 Jun 2009 06:33:51 GMT
Even though this helps you can imagine that this still requires a whole roundtrip to the server which even though will have a short response, it can still have a significant impact if rendering of the page is waiting for it, as in the case of a CSS file that the browser needs to resolve to display correctly the page or an <img> tag that does not include the dimensions (width and height attributes) and so requires the actual image to determine the required space (one reason why you should always specify the dimensions in markup to increase rendering performance).