There are many situations
where it is acceptable for a page response to be a little stale if this
brings significant performance advantages. Want an example? Think of an
e-commerce application and its set of pages for the products catalog.
These pages are relatively expensive to create because they could
require one or more database calls and likely some form of data join.
All things considered, a page like this could easily cost you a few
million CPU cycles. Why should you regenerate this same page a hundred
times per second? Product pages tend to remain the same for weeks and
are rarely updated more than once per day. Whatever the refresh rate is
for the pages, there’s little value in regenerating them on a
per-request basis.
A much better strategy is to create the page
once, cache it somewhere, and give the page output 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.
ASP.NET page output caching is the feature that
allows you to cache page responses so that following requests can be
satisfied without executing the page; instead, the requests are
satisfied by simply returning the cached output. Output caching can take
place at two levels—entire pages or portions of the page. Page caching
is smart enough to let you save distinct output based on the requesting
URL, query string or form parameters, and even custom strings.
Easy to set up and terrifically effective, output caching can either be configured declaratively through the @OutputCache directive or programmatically through an API built around the HttpCachePolicy class.
Important
Page output caching is
simply a way to have the application serve more pages more quickly. It
has nothing to do with sophisticated caching strategies or elegant code
design. In other words, it will enable your application to serve pages
faster, but it won’t necessarily make the application more efficient and
scalable. With page output caching, you can certainly reduce the
workload on the server as pages are cached downstream. Finally, be aware
that page output caching works only for anonymous content. Requests for
cached pages are served by Internet Information Services (IIS) 6.0
directly or by the ASP.NET worker process under IIS 5.0. In any case, a
page request never reaches stages in the ASP.NET pipeline where it can
be authenticated, which is a strategy employed to prevent access to
protected content. |
1. The @OutputCache Directive
Caching the output of a page is as easy as defining an @OutputCache directive at the top of the page. The directive accepts a handful of attributes, a couple of which—Duration and VaryByParam—are mandatory. The Duration attribute indicates in seconds how long the system should cache the page output. The VaryByParam attribute allows you to vary the cached output depending on the GET query string or form POST parameters. The following declaration will cache the page for one minute regardless of any GET or POST parameters:
<%@ OutputCache Duration="60" VaryByParam="None" %>
For frequently requested pages and relatively static pages, the @OutputCache
directive is a real performance booster. With a shorter duration, even
limited to one second or two, it provides a way to speed up the entire
application.
The @OutputCache
directive consists of six attributes that indicate the location of the
cache, its duration, and the arguments to use to vary page caching. The
list of supported attributes is shown in Table 1. Note that the directive can be applied to both pages (.aspx) and user controls (.ascx). Some of the attributes are valid in one case but not the other.
Table 1. Attributes of the @OutputCache Directive
Attribute | Applies to | Description |
---|
CacheProfile | Page | Associates a page with a group of output caching settings specified in the web.config file (More later). Not supported in ASP.NET 1.x. |
Duration | Page, User control | The time, in seconds, that the page or user control is cached. |
Location | Page | Specifies a valid location to store the output of a page. The attribute takes its value from the OutputCacheLocation enumeration. |
NoStore | Page | Indicates whether to send a Cache-Control:no-store header to prevent additional storage of the page output. Not supported in ASP.NET 1.x. |
Shared | User control | Indicates whether the user control output can be shared with multiple pages. It is false by default. |
SqlDependency | Page, User control | Indicates
a dependency on the specified table on a given SQL Server database.
Whenever the contents of the table changes, the page output is removed
from the cache. Not supported in ASP.NET 1.x. |
VaryByControl | User control | A
semicolon-separated list of strings that represent properties of the
user control. Each distinct combination of values for the specified
properties will originate a distinct copy of the page in the cache. |
VaryByCustom | Page, User control | A
semicolon-separated list of strings that lets you maintain distinct
cached copies of the page based on the browser type or user-defined
strings. |
VaryByHeader | Page | A semicolon-separated list of HTTP headers. |
VaryByParam | Page, User control | A semicolon-separated list of strings representing query string values sent with GET method attributes, or parameters sent using the POST method. |
Note that the VaryByParam
attribute is mandatory. If you omit it, a runtime exception is always
thrown. However, if you don’t need to vary by parameters, set the
attribute to None. The empty string is not an acceptable value for the VaryByParam attribute.
Choosing a Duration for the Page Output
When the output caching service is active on a page, the Duration
attribute indicates the number of seconds that the caching system will
maintain an HTML-compiled version of the page. Next, requests for the
same page, or for an existing parameterized version of the page, will be
serviced while bypassing most of the ASP.NET pipeline. As mentioned,
this process has two important repercussions—no authentication is
possible and no code is run, meaning that no page events are fired and
handled and no state is updated. The implementation of output caching
varies with the ASP.NET process model in use.
With the IIS 5.0 process model, any request for an ASP.NET page is always handed over to the worker process, assigned to an HttpApplication object, and processed by the pipeline. The ASP.NET pipeline includes an HTTP module named OutputCacheModule that captures two application-level events related to output caching—ResolveRequestCache and UpdateRequestCache. In particular, the module uses the ResolveRequestCache
event handler to short-circuit the processing of requests for pages
that have been cached. In the end, the request is hooked by the HTTP
module and served by returning the copy of the page stored in the cache.
When the page is being generated, OutputCacheModule grabs the output of the pages marked with the @OutputCache directive and stores it internally for further use. The output of the page is stored in a private slot of the ASP.NET Cache object. Setting the Duration
attribute on a page sets an expiration policy for the HTTP response
generated by the ASP.NET runtime. The output is cached by the module for
exactly the specified number of seconds. In the meantime, all the
incoming requests that hit one of the cached pages are serviced by the
module rather than by the ASP.NET pipeline.
With the IIS 6.0 process model, the output
caching mechanism is integrated in the Web server, resulting in much
better performance and responsiveness, thanks to the IIS 6.0 kernel caching.
When enabled, this feature makes it possible for IIS to intercept the
output of a page generated by ASP.NET. A copy of the page output is then
cached by the IIS kernel. Incoming requests for an ASP.NET page are
filtered by a kernel-level driver (http.sys)
and examined to see whether they match cached pages. If so, the output
is served to callers directly from kernel-level code without even
bothering the worker process and the ASP.NET pipeline. If you have any
ASP.NET applications and are still considering an upgrade to IIS 6.0,
this is a great reason to do it as soon as possible. Note that this
facility in IIS 6.0 is used by ASP.NET since version 1.1 to host the
output cache. So, when using the output cache directive in ASP.NET 1.1
and ASP.NET 2.0 (or newer) applications, your responses are being served
from the kernel cache. See the sidebar “Inside IIS 6.0 Kernel Caching” for more performance details.
A fair value for the Duration
attribute depends on the application, but it normally doesn’t exceed 60
seconds. This value usually works great, especially if the page doesn’t
need to be updated frequently. A short duration (say, 1 second) can be
useful for applications that claim live data all the time.
IIS 6.0 employs an ad hoc component to cache
the dynamically generated response to a request in the kernel. 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. What’s the performance gain you can get?
According to the numbers provided with the IIS
6.0 technical documentation, an application using the kernel cache
returns a throughput of over ten times the throughput you would get in
the noncached case. Additionally, the latency of responses is
dramatically better. The following table compares caching in IIS 6.0
kernel mode and user-mode caching to caching as implemented by the
ASP.NET runtime in IIS 5.0. (Note that TTFB stands for “time to first
byte” while TTLB stands for “time to last byte” for serving the page in
question.)
The numbers in the preceding table provide you
with an idea of the results, but the results will vary according to the
amount of work and size of the response. The bottom line, though, is
that leveraging the kernel cache can make a dramatic difference in the
performance of an application. The great news for ASP.NET developers is
that no code changes are required to benefit from kernel caching, except
for the @OutputCache directive.
On a high-volume Web site, an output cache
duration of only one second can make a significant difference for the
overall throughput of a Web server. There’s more to know about kernel
caching, though. First and foremost, kernel caching is available only
for pages requested through a GET verb. No kernel caching is possible on postbacks.
Furthermore, pages with VaryByParam and VaryByHeader
attributes set are also not stored in the kernel cache. Finally, note
that ASP.NET Request/Cache performance counters will not be updated for
pages served by the kernel cache.
|
Choosing a Location for the Page Output
The
output cache can be located in various places, either on the client
that originated the request or the server. It can also be located on an
intermediate proxy server. The various options are listed in Table 2. They come from the OutputCacheLocation enumerated type.
Table 2. Output Cache Locations
Location | Cache-Control | Description |
---|
Any | Public | The HTTP header Expires is set according to the duration set in the @OutputCache directive. A new item is placed in the ASP.NET Cache object representing the output of the page. |
Client | Private | The output cache is located on the browser where the request originated. The HTTP header Expires is set according to the duration set in the @OutputCache directive. No item is created in the ASP.NET Cache object. |
DownStream | Public | The
output cache can be stored in any HTTP cache capable devices other than
the origin server. This includes proxy servers and the client that made
the request. The HTTP header Expires is set according to the duration set in the @OutputCache directive. No item is created in the ASP.NET Cache object. |
None | No-Cache | The HTTP header Expires is not defined. The Pragma header is set to No-Cache. No item is created in the ASP.NET Cache object. |
Server | No-Cache | The HTTP header Expires is not defined. The Pragma header is set to No-Cache. A new item is placed in the ASP.NET Cache object to represent the output of the page. |
ServerAndClient | | The data can be stored at the origin server (creating an item in the ASP.NET Cache) or on the receiving client. |
A page marked with the @OutputCache directive also generates a set of HTTP headers, such as Expires and Cache-Control.
Downstream proxy servers such as Microsoft ISA Server understand these
headers and cache the page along the way. In this way, for the duration
of the output, requests for the page can be satisfied even without
reaching the native Web server.
In particular, the Expires
HTTP header is used to specify the time when a particular page on the
server should be updated. Until that time, any new request the browser
receives for the resource is served using the local, client-side cache and no server round-trip is ever made. When specified and not set to No-Cache, the Cache-Control HTTP header typically takes values such as public or private. A value of public means that both the browser and the proxy servers can cache the page. A value of private prevents proxy servers from caching the page; only the browser will cache the page. The Cache-Control is part of the HTTP 1.1 specification and is supported only by Internet Explorer 5.5 and higher.
If you look at the HTTP headers generated by ASP.NET when output caching is enabled, you’ll notice that sometimes the Pragma header is used—in particular, when the location is set to Server. In this case, the header is assigned a value of No-Cache,
meaning that client-side caching is totally disabled both on the
browser side and the proxy side. As a result, any access to the page is
resolved through a connection.
Note
To be precise, the Pragma header set to No-Cache disables caching only over HTTPS channels. If used over nonsecure channels, the page is actually cached but marked as expired. |
Let’s examine the client and Web server caching configuration when each of the feasible locations is used.
Any
This is the default option. This setting means that the page can be
cached everywhere, including in the browser, the server, and any proxies
along the way. The Expires header is set to the page’s absolute expiration time as determined by the Duration attribute; the Cache-Control header is set to public, meaning that the proxies can cache if they want and need to. On the Web server, a new item is placed in the Cache
object with the HTML output of the page. In summary, with this option
the page output is cached everywhere. As a result, if the page is
accessed through the browser before it expires, no round-trip is ever
made. If, in the same timeframe, the page is refreshed—meaning that
server-side access is made anyway—the overhead is minimal, the request
is short-circuited by the output cache module, and no full request
processing takes place.
Client The page is cached only by the browser because the Cache-Control header is set to private. Neither proxies nor ASP.NET stores a copy of it. The Expires header is set according to the value of the Duration attribute.
DownStream The page can be cached both on the client and in memory of any intermediate proxy. The Expires header is set according to the value of the Duration attribute, and no copy of the page output is maintained by ASP.NET.
None Page output caching is disabled both on the server and on the client. No Expires HTTP header is generated, and both the Cache-Control and the Pragma headers are set to No-Cache.
Server The page output is exclusively cached on the server, and its raw response is stored in the Cache object. The client-side caching is disabled. No Expires header is created, and both the Cache-Control and Pragma headers are set to No-Cache.
ServerAndClient
The output cache can be stored only at the origin server or at the
requesting client. Proxy servers are not allowed to cache the response.
Adding a Database Dependency to Page Output
The SqlDependency attribute is the @OutputCache directive’s interface to the SqlCacheDependency class that we discussed earlier. When the SqlDependency attribute is set to a Database:Table
string, a SQL Server cache dependency object is created. When the
dependency is broken, the page output is invalidated and the next
request will be served by pushing the request through the pipeline as
usual. The output generated will be cached again.
<% @OutputCache Duration="15" VaryByParam="none"
SqlDependency="Northwind:Employees" %>
A page that contains this code snippet has its
output cached for 15 seconds or until a record changes in the Employees
table in the Northwind database. Note that the Northwind string here is
not the name of a database—it’s the name of an entry in the <databases>
section of the configuration file. That entry contains detailed
information about the connection string to use to reach the database.
You can specify multiple dependencies by separating multiple Database:Table pairs with a semicolon in the value of the SqlDependency attribute.
Note
A user control is made cacheable in either of two ways: by using the @OutputCache directive, or by defining the PartialCaching attribute on the user control’s class declaration in the code-behind file, as follows: [PartialCaching(60)]
public partial class CustomersGrid : UserControl {
...
}
The PartialCaching attribute allows you to specify the duration and values for the VaryByParam, VaryByControl, and VaryByCustom parameters. |