Practical .NET
High-Performance ASP.NET Caching
Crafting a caching strategy is critical to building effective Web apps. It's only possible when you know what options are available and how to integrate them.
There are two separate places where your application can be slowed to a crawl: In the jump from the browser to the Web server; and at the server, from the Web server to the database server (see Figure 1). While there are numerous programming tricks you can use to speed up your application, you'll get the best results by reducing those trips through intelligent caching of the application's data.
 [Click on image for larger view.] |
| Figure 1. A typical Web application looks like the one at the top: multiple time wasting trips to the Web Server and, from there, to the database server. Smarter, more scalable applications cache data at the client and the Web server farm as part of an integrated caching strategy. |
In addition, as all Web developers know, managing state (keeping track of what the user has done in previous requests in the same session) also involves caching key pieces of data for later reference. Reducing the data cached at the server reduces the demands placed on the resource shared by all users -- the Web server. Fewer requests made to your servers and less memory used on the Web server means your application supports more users while processing their requests faster.
Caching isn't the only way to meet those two goals, of course: You can also scale your application out to a server farm. However, if you've used caching to boost performance on a single server, you don't want to do a major rewrite when moving to a farm.
Throwing everything into the Session object and using a Session server isn't the answer to any of these problems. There are a wide variety of tools for caching data, with each of these tools solving a particular problem. If you're not taking advantage of all of them -- and understanding when they're appropriate -- your site won't be performing as it should. Those tools include some new, standards-based technology at the client, effectively integrating the tools ASP.NET gives you, and exploiting some new Microsoft technology.
Client-Side Caching
To reduce trips to the server, you can store data at the client where your JavaScript code can access it. AJAX-based solutions access Web services that eliminate the need for postback and rebuilding the whole page -- but even with AJAX you're still making that jump to the Web server; and the Web Service you're calling is probably jumping over to the database server. Caching ensures that once you retrieve the data, you keep it at the client to eliminate later trips.
While leveraging client-side data is dependent on your willingness to write client-side code, the benefits are huge. Client-side data has no server-side footprint taking up memory between page requests. And by providing access to data that would otherwise require a trip to the server, client-side data reduces both trips to the server (which the user will appreciate) and processing on the server (which you'll appreciate).
Ideally, the first time a user hits your site, you'd download to the client all the data it would ever need. Your only trips back to the server would be to perform updates. In real life, though, data is volatile, and over time data stored on the client will grow stale -- any caching strategy needs a more sophisticated solution. While you can use hidden fields to store data on a page-by-page basis, a more powerful set of tools for caching data at the client is the W3C Web Storage standard.
Web Storage -- Not
This standard was originally part of the HTML5 set of specifications, but has since been split off. Unlike many HTML5 specifications, Web Storage is widely implemented (available in Internet Explorer 8, Firefox 2.x, Safari 4, Google Chrome 5, and Opera 10.50 -- basically, any browser version released after 2010). Web Storage has to be the worst name ever given to a technology, because the one thing it doesn't do is store data on the Web -- Microsoft, for instance, refers to the technology by its alternate name: DOM storage.
Web/DOM Storage provides two mechanisms for storing data at the client using JavaScript code running in the browser. One mechanism (local storage) supports long-term storage at the client that survives the browser being shut down. Local storage ensures that pages can only access data saved from pages in the same domain.
The other mechanism (session storage) automatically takes care of disposing your data when the tab in a window is closed. It also ensures that data retrieved in two different tabs is kept separate (something that cookies won't do, for instance). For both mechanisms, the data format is associative -- name/value pairs -- which means you won't be doing any data analysis with these tools. There's a separate specification for a more powerful mechanism (Indexed Database API) and a de facto standard called Web SQL Database that also provides more flexible storage, but only Web Storage can be used reliably.
Your caching strategy will need to deal with three kinds of client-side data:
- Data used across multiple pages by a single user (data you'd normally stuff in the Session object on the server).
- Data personal to the user. This data is updated only by the user. That data can be updated on the client when the user changes it, and stuffed into hidden fields to be returned to the server when needed for server-side processing. You'll need to keep a copy of that data on the server, however (to support the user as they move from one computer to another or for data analysis).
- "Semi-volatile" data that doesn't change very frequently: everything from lists of countries to display in drop-down lists to your company's products list.
Using the Storage Classes
As an example of how to use client-side data for temporary storage, assume a user's attempting to buy some product on your site. You can store all that transaction's information in the Session object where it's inaccessible to client-side code, will take up space in memory between page requests, and will hang around for 20 minutes after the user finishes with your site. Or you could store the data in session storage and, on a page-by-page basis, embed it in hidden fields to get it back to the server when needed for server-side processing.
Fortunately, adding new data to session storage is easy: simply set a property with a name of your own to the sessionStorage object. The property name will be used as the key for the value stored in the property. Transferring data entered in a textbox to session storage can be as simple as the following combination of HTML and JavaScript. This code stores the value of the textbox in session storage under the key "cName" when the focus shifts from the textbox:
<asp:TextBox ID="CustName" runat="server" onblur="sessionStorage.cName=this.value;">
Retrieving the cached data and putting it in some other control looks like this:
if (sessionStorage.cName != null)
{
window.document.getElementById("HiddenNameProcessed").value =
sessionStorage.cName;
}
This code would successfully retrieve data that was stored by code executing in any page from the same domain (such as phvis.com) or subdomain (such as www.phvis.com) running in the same window/tab -- in other words, from any page in the user's session.
Handling user data and semi-volatile data is only slightly more complex. I'll assume that you'll deliver your client-side data through a set of Web services called by JavaScript code in the client. Making that assumption, once the page is ready, I check to see if the needed data is stored locally by checking some arbitrary property that I'll eventually create on the localStorage object:
var store = localStorage;
if (store.lastRetrieved != nothing)
{
My next step is to determine if the local data is out of date; if it isn't, I'll retrieve the latest version of the data. To do that, I call a Web service that returns the necessary data. When calling that service, in addition to passing any parameters the service requires, I also pass some value that indicates the state of the data on the client so the Web service can determine if the client's data is stale (of course, if I haven't retrieved the data before then this value will be null). The simplest value might just be the last retrieved date, but an alternative is to use the timestamp value from a database row. Using a date allows me to reduce checking on the client by not requesting the data if I've updated local storage within, for instance, the last hour.
The following code uses the jQuery getJson function to call the Web service in a RESTful kind of way. You could, however, just as easily use ScriptManager's support for calling Web services. The querystring for this request includes the user's ID and the value indicating the state of the data on the client:
$.getJSON(
"http://myserver.com/UserServices/GetUser?uId= + $("#username").val() +
"&lastRetrieve=" + localStorage.lastRetrieved,
function (returnValue)
{
Once I retrieve the data, I update local storage with properties on the returned object that contain the user's data and the value representing the state of the data:
store.data = returnValue.data;
store.lastRetrieved = returnValue.lastRetrieved;
There are limitations to the amount of data you're allowed to store per domain, but they're very forgiving: 10MB in Internet Explorer and 5MB in most other browsers. Both local and session storage have a remainingSpace method that returns the amount of space available for use.
On both storage objects, you can also use their length method to retrieve the number of items in storage, the key method to retrieve the key for a value by position, the removeItem method to remove an item from storage, and the clear method to clear out everything in the storage for your domain. The getItem and setItem methods allow you to retrieve or set values when passed a key instead of hardcoding the key names as property values. This code, for instance, retrieves all the items in local storage:
var key;
for (i = 0; i < sessionStorage.length; i++)
{
key = sessionStorage.key(i);
window.alert(sessionStorage.getItem(key));
}
As with cookies, everything must be stored as a string value.