In this article, I'll review browser caching basics and the techniques you can use to modify cache settings for ASP.NET pages.
Browser Caching Rules
Note that browsers won't cache any responses that explicitly prohibit caching. Browsers also won't cache responses that come from a secure HTTPS channel or that require authentication. Aside from that, any response is cacheable.
For each requested URL, a browser always looks to the local cache first. If the requested URL doesn't have a match in the local cache, the browser sends the request on to the server. If a match is found, the browser checks whether the cached output of the requested resource is still valid. A valid output is any output that has not expired. If a valid output is found for a requested URL, the request is served directly from the cache without contacting the server.
The browser uses information originally contained in HTTP response headers to determine whether a given output is fresh or stale. The Expires HTTP header, for example, indicates the absolute expiration date of the resource. The max-age HTTP header indicates for how long the response has to be considered fresh since the time of download.
If the response is stale, but a clear expiration date or an age was specified, the browser will ask the original server to validate the representation. In this case, the browser request will include the If-Modified-Since request header set to the value of the expiration date. The status code will be HTTP 304 (not modified), if nothing has changed since—meaning that the currently cached resource is still the most up to date. In this case, the request is still served from the cache; otherwise, a new request is made to get a fresher response.
These simple rules express the behavior of any web caches, including not only browser caches but also downstream proxy caches. If two types of resources (i.e., ASPX and JS files) behave differently, the trick is in the HTTP response headers they are downloaded with.
Behavior of ASPX Resources
An ASP.NET page is not a readily cacheable resource because its content might be different, even when the requesting URL is the same. An image that represents a company's logo, or a script file, is a much more static type of resource and inherently more cacheable. The default behavior of the ASP.NET page results from the following similar list of HTTP headers, the most important of which is the Cache-Control header:
Cache-Control private Content-Type text/html; charset=utf-8 Server Microsoft-IIS/7.5 X-AspNet-Version 4.0.30319 X-Powered-By ASP.NET Date Wed, 08 Dec 2010 20:09:06 GMT Content-Length 12315
A value of private in the Cache-Control header indicates that an ASP.NET page is cacheable by web browsers, but not by downstream proxy servers. In addition, the lack of Expires and max-age headers indicates that an ASP.NET page has no expiration set; therefore, it's always stale. In addition, the browser has no information to submit to the server through the If-Modified-Since header. The bottom line is that any request you make for an ASPX resource will always result in an immediate new fetch from the server as if the page was never cached.
You can change the caching settings for any ASPX page at your leisure by setting HTTP headers explicitly, both through the IIS management console and programmatically. All things considered, however, probably the simplest and equally effective way of changing cache settings for ASP.NET pages is by using the @OutputCache directive.
On the other hand, static resources typically are served with a relatively long lifetime, with the goal of keeping them in the cache as long as possible. Developers and web masters are ultimately responsible for deciding the maximum age allowed for a given set of resources. The set of response headers being used by web servers might differ quite a bit.
In IIS, for example, HTTP response headers for static resources commonly include the ETag header, which is a hash for the content of a given resource. Typically, when a browser receives an ETag, it caches the hash with the resource and then uploads the ETag to the server when a new request comes in. The ETag value is uploaded packed in an If-None-Match request header. At this point, the web server can either return an HTTP 304 (not modified) status code or a fresher resource with an updated ETag.
Making ASP.NET Pages Cacheable
Not all ASP.NET pages need be re-created for every request. Think, for example, of the set of pages for the products catalog in an e-commerce application. They certainly won't change every few seconds. So a better strategy is to create the page once, cache it somewhere (client and/or proxy), and give the page response a maximum duration. Once the cached page becomes stale, the first incoming request will be served in the standard way, running the page's code, and the new page output will be cached for another period until it also becomes stale.
The @OutputCache directive is the central console from where you control the cache-related behavior of the page. Here's a common way of using the directive at the top of the ASPX page:
<%@ OutputCache Duration="60" VaryByParam="none" %>
Figure 1 shows the entire list of attributes supported by ASPX pages, as of ASP.NET 4.
Figure 1: Attributes of the OutputCache directive
Note that the VaryByParam attribute is mandatory for ASP.NET pages; if you omit the attribute, you'll get a runtime exception. You set the attribute to none, if you have no interest in caching different versions of the page based on parameters.
Among other things, you use the @OutputCache directive to decide where the page output should be cached. In general, it can go in three different locations, one not necessarily excluding the other. The page can be cached on the client (the browser cache), on the IIS web server, or by an intermediate proxy server. The Location attribute says it all; possible values for the attribute come from the OutputCacheLocation enumerated type (see Figure 2).
Figure 2: Feasible values for the Location attribute
Note that if you don't assign any value to the Location attribute, the Cache-Control header is set to Private. When the Cache-Control header is public, ASP.NET emits the header max-age set to the same value as the duration attribute. The value of No-Cache assigned to the Cache-Control HTTP header instructs the browser to check with the server regarding the freshness of the page before serving it. However, in combination with Expires set to -1, it indicates that the page is stale and subsequently needs be fetched again. The net effect is the same because the page was never cached. The No-Store value instructs the browser not to save the resource locally.
The @OutputCaching directive is more popular for its ability to cache the output of the page on the web server. Server-side caching is yet another level of page output caching specific to ASP.NET. A special component—the page output provider—will capture the output of a page and cache it somewhere on the server. The default provider caches pages inside the ASP.NET Cache object in the memory space of the worker process (and machine) that is currently serving the request.
This solution is not ideal if you have a web farm where there's no guarantee that subsequent requests for the same page are served by the same machine. If you're running a web farm, you might want to consider replacing the default provider with the output cache provider made available by the AppFabric Caching services.
In IIS 6 and newer versions, you can tell IIS to cache the page output for you without involving the ASP.NET runtime. This feature has tremendous potential and can dramatically improve the performance of a web server, as long as enough of the content being served is cacheable.
The great news for ASP.NET developers is that no code changes are required to benefit from kernel caching, except for the @OutputCache directive. You enable kernel caching administratively from within IIS Manager. When both output caching and IIS kernel caching are enabled, a kernel-level driver in IIS intercepts any incoming requests and, if the response was previously cached, it serves them by directly flushing the cached data from wherever the output provider in use had stored it. Because this happens at the kernel level, there's no need to make any context switch to user mode—which means a remarkable performance improvement in the order of one-tenth of the time it would take in classic user mode.
All browsers cache any resources except for those delivered over HTTPS and authenticate and treat them in exactly the same way. If the behavior you observe is different, that is due to the HTTP headers with which the response was originally received by the browser. Such headers ultimately drive the behavior of the browser. By default, ASP.NET pages are cached on the browser, but the pages look constantly stale to the browser. For this reason, they are always fetched fresh from the web server. You use the @OutputCache directive to change these settings.
Dino Esposito ( [email protected]), an architect at IDesign, specializes mainly in ASP.NET, AJAX, and RIA solutions. Dino co-authored Microsoft .NET: Architecting Applications for the Enterprise (Microsoft Press) and is the author of Programming ASP.NET MVC 2 (Microsoft Press).