Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Website Performance with ASP.NET - Part 2 - Reduce Time to First Byte

0.00/5 (No votes)
22 Mar 2013CPOL4 min read 17K  
This post shows how the time to first byte of ASP.NET pages can be reduced by profiling the code and implementing data and output caching.

In the previous post, we learned that the performance of the example application could use some improvements and already identified measures which might help. Now, it’s time to start exploring the options we have implementing these.

Profiling the Server Side Code

The server side performance is not the focus of this series. It is often the first (or even only) part which many ASP.NET developers focus on when thinking about performance. When looking at page load times, it is often overrated as it is only one of many factors to consider. Nevertheless, should you take a look at your time to fist byte (i.e. execution time on the server + transport). Especially your HTML should be delivered as fast as possible as all dependent resources (images, css, js) are not loaded until the HTML has been parsed by the browser.

From the tests performed in the previous post, we already know that the time to first byte in our example application is bad. Now we need to find out what is causing this. Tools like dotTrace, ANTS Profiler or the Visual Studio Performance Profiler can help you to identify the performance hot spots of you code. I used dotTrace to profile the sample application and it pointed to a single method consuming almost 90% of the request time.

Image 1

Ok, I have to admit, for demonstration purposes, I cheated a little bit. If you take a look into the source code of the SampleProductDataSource in the unoptimized branch, you will find these lines:

JavaScript
private static Product[] GetProducts(CultureInfo culture)
{
	// Simulate slow DB Access
	Thread.Sleep(500);
	...
}

That’s obviously nothing you should find in production code, but a slow databases and/or inefficient data access code are unfortunately not uncommon. You can, of course, tune the data access code and/or the database. It is, however often times more efficient to prevent the data access rather than optimizing it. You usually don’t need to get the latest data for every requests. Stale data is often acceptable. Take the shop example: There is no reason to load static product information hundred or thousand times per hour, if the information is updated only once per day.

Data Caching

A simple fix for relatively static data like the product information is to use a data/object cache. The .NET Framework includes the System.Runtime.Caching.MemoryCache which implements a simple and thread-safe in-memory cache. There are many more sophisticated cache implementations available, both free and commercial. But the MemoryCache is a good choice for simple scenarios. You can add items with a sliding expiration policy (e.g. 30 minutes after the last usage) or an absolute expiration policy (e.g., expire today at 11:59 pm). The optimized version of the SampleProductDataSource uses the MemoryCache to store the product information for one hour.

JavaScript
private static readonly MemoryCache Cache = new MemoryCache("storeProductData");

private static Product[] GetProducts(CultureInfo culture)
{
	var cachedProducts = (Product[])Cache.Get(culture.ToString());

	if (cachedProducts != null)
	{
		return cachedProducts;
	}

	...

	Cache.Add(culture.ToString(), productsArray, new CacheItemPolicy {
  AbsoluteExpiration = DateTimeOffset.UtcNow.AddHours(1) });
	
	return productsArray;
}

Output Caching

If not only some data but (almost) complete pages are relatively static (e.g. a product details page) the ASP.NET Output Cache offers an easy to use mechanism to cache the HTML output of a page. For ASP.NET MVC, you can add the OutputCacheAttribute to an action method to use the output cache. For ASP.NET WebForms pages, use the @OutputCache declaration. The output cache can be configured with parameters, to set the duration for which the content should be cached, as well as the storage location (browser, proxy, server). Those parameters can be set for every action/page or can be stored in profiles within the web.config.

The optimized version of the example application allows caching of the product details action for 2 hours on the server, proxies and browsers:

JavaScript
[OutputCache(Duration = 7200)]
public virtual ActionResult ShowProductDetails(CultureInfo culture, string productName)
{
	ProductDetailsViewModel productDetailsViewModel = 
           this.productService.GetProductDetails(culture, productName);
	return View(productDetailsViewModel);
}

The output cache automatically sets the corresponding cache-control headers to inform clients and proxies how long the results may be served from cache without issuing another request to the server (I will provide more details on cache-control header later in the series).

Before implementing caching, it is important to separate static content from dynamic content. Using the output cache only makes sense if the dynamic portion of the content is relatively small. Then you can render the static content within the main action and cache the result. The dynamic information can then be added by using a separate Ajax request and some JavaScript. The example application caches the content pages themselves and adds only information about the current shopping cart content using JavaScript.

JavaScript
$.getJSON(cartServiceUrl, null, function(cartSummary) {
	$(uiConfigValues.itemNumberSelector).text(cartSummary.NumberOfCartItems);
	$(uiConfigValues.totalAmountSelector).text(cartSummary.FormattedTotalCartAmount);
});

Finding a caching strategy for your application can take some time, but with the above techniques, you should be able to cover most scenarios.

In the next post, we will start digging into more front-end specific topics by looking at ways to reduce the number of HTTP requests.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)