Practical ASP.NET

Optimize ASP.NET Performance

Take advantage of these performance tips, and make your ASP.NET applications fly whether you use version 1.1 or 2.0.

Technology Toolbox: Visual Basic, ASP.NET

It's axiomatic that everyone wants to create applications that run faster and use less memory. In an ideal world, the best thing to do is to sit down and take the time to rearchitect your application so it meets your performance goals. Rarely is the best approach implementing a series of minor tweaks to fix performance bottlenecks you discover after you've written the application.

In the real world, though, you often find yourself tracking down performance bottlenecks and coding minor tweaks to address them. I'll give you some easy-to-implement tips that can help you address some common coding performance problems in your ASP.NET applications. Some of these tips will help you avoid the problem in the first place, whereas others might help you find and fix a problem after you see it. Nothing is free, unfortunately, and most of these tips come with some kind of trade-off. Sometimes that trade-off is memory for speed (or vice versa), and sometimes that trade-off is accepting that your program behaves differently. Sometimes rearchitecting your application doesn't seem like such a bad idea after all.

One of the most important things you can do to improve performance in an ASP.NET application is to reduce the number of round trips to the server. Don't request the current page if you don't have any processing to perform on the server before transferring the user to another page. For example, I once had a client whose most frequently used button on the most frequently used page contained a single line of code that did exactly one thing: It redirected the user to the next page. In this circumstance, you can dispense with the button and simply add a Hyperlink control to the page that causes the browser to request the next page directly. Your server will have one less page to process.

Sometimes you use a button and a line of code because the next page needs some data from the current page. In ASP.NET 2.0, however, you can avoid requesting the current page and post the current page's data to the server by using cross-page posting. All you have to do is set the PostBackUrl property of your button to the URL of the next page. On the new page, use the PreviousPage property to retrieve the data you need from the current page. This code retrieves the value of a control called FirstNameTextBox from the previous page:

Dim Name As String 
Name = CType(Me.PreviousPage.FindControl( _
	"FirstNameTextBox"), TextBox).Text

You also can reduce the overhead of transferring the user from one page to another. If your user doesn't need to bookmark a page, use the Transfer method of the Server object to send the user a new page instead of the Redirect method of the Response object. The Redirect method requires two round-trips to the server to get the user to the next page. The Transfer method simply sends the user to the new page, eliminating the overhead of a second request to the server (see Figure 1).

Note that there is a usability cost to employing the Transfer method. You can use the Transfer method only to send the user to a page in the same site. More importantly, the browser doesn't see the switch in pages, so the address bar of the browser reflects the URL of the original page. This can cause a complication when bookmarking the page because the user is actually bookmarking the page you transferred the user from, rather than the page the user sees. Similarly, a user refreshing the page is requesting a new version of the page specified in the address box, rather than the displayed page.

Reduce the Server's Load
It's obvious, but the less work you do on the server, the faster your server will run. Unless you take advantage of some server-side feature of a control, you should consider using the equivalent HTML control for any server-side code. For instance, do the labels on your page need to be ASP.NET server-side labels? If not, use the HTML Label control, which requires considerably less processing on the server. If all you use the DataGrid control for is to display data, you should look at switching to a Table control. You can avoid all the overhead of the DataGrid for the cost of a few lines of code to generate the rows and columns for your data. Faster yet, you can write the Table tags yourself and use a Literal control to display your HTML. It's more work for you at design time, but less work for your server on every request for the page at run time.

The more processing that you do on the client with client-side code, the less processing you'll have to do on the server. ASP.NET 1.1 includes several methods for adding client-side code to your page dynamically. In ASP.NET 2.0, the tools are expanded to make it even easier to insert JavaScript into your page. Visual Studio 2005 also provides better support for client-side programming. Setting the GridView's EnableSortingandPagingCallbacks property (or the DetailView's EnablePagingCallbacks property) causes all sorting and paging to be performed on the client rather than on the server.

In ASP.NET 2.0, you can use client-side code and callbacks to avoid posting the whole page back to the server for processing (see Additional Resources). A client-side callback lets you send only the data you need back to the server and then retrieve the result of your server-side processing without posting a whole page. It's not Asynchronous JavaScript And XML (AJAX), but it works, and it reduces the overhead on your server.

You should also consider turning off the ViewState for any control whose properties you won't be altering from your code. Turning off the ViewState for a control reduces the data traveling between your server and the browser.

Turning off the ViewState does come with a cost, however. It can result in your control's client-side events firing in unexpected ways. For example, if you turn off the ViewState for a textbox, the TextChanged event fires whenever the Text property's value is different from its default value, not every time a user changes the Text property. Avoiding code that depends on client-side events solves this problem. In ASP.NET 2.0, controls can store essential data in a section of the ViewState that is never disabled, so clever control authors can minimize unexpected side effects.

Of course, you should consider turning off anything you don't need. For example, ask yourself if you need authentication. ASP.NET enables authentication by default in your site's web.config file, but you should turn it off if you're not using it (set the authentication element's mode attribute to None). In ASP.NET 2.0, you should also make sure you haven't enabled personalization inadvertently. Even if you are using the ASP.NET 2.0 personalization feature, ask yourself whether you need to support personalization for anonymous users. If not, turn that off by setting the Enabled property on the anonymousIdentification element to False. The less your server does, the faster everything else will run.

Reduce Trips to the Database
It's important to reduce any round trips needed by your application, not just round trips to the server. For example, you can keep data on your Web server instead of the database server by storing any data that you retrieve and might need again in the cache. And never, ever store anything bulky in the Application object—put any bulky items in the Cache object.

Note that items in the Cache object are discarded automatically when your server starts to run out of memory. It makes sense to put as much as you can in the ASP.NET Cache object because the memory this object uses will free up automatically when (and if) it's necessary. For example, consider that table of states and provinces that you keep retrieving from the database to display in listboxes all over your site. Instead, you might retrieve that table into a DataSet stored in the Cache object and bind all your listboxes to that table in the DataSet. Yes, the DataSet takes up memory, but the cache discards the DataSet if memory runs short.

If you use this approach, you must add code to check whether the DataSet is still present in the Cache object before you use the DataSet. But the worst that can happen is that you must go back to the database for your data. You're no worse off than before, and you've probably saved yourself dozens of trips to the server when memory was available. If you need some information to know which item to retrieve from the database when the Cache object dumps your object (a customer number, for instance), store that small amount of data in the Application or Session objects.

Using the Cache object this way gives you an in-memory data store that adapts automatically to changing conditions. The items you haven't used for the longest period of time are removed from the Cache object first, so you're guaranteed that your most frequently used objects will stick around the longest. Your code only adds back to the cache the items your program uses, so only the objects that you need creep back into memory. This approach is extremely efficient.

Exploiting the cache is even easier in ASP.NET 2.0 if you use the SqlDataSource object. You can turn on caching for your data by setting the EnableCaching property on the SqlDataSource to True. In this case, you need to ensure that the data you cache doesn't get out of date relative to the data in the database. Fortunately, there's an easy fix for this: Simply make sure the SqlDataSource discards cached data after a specified time by setting the DataSource's CacheDuration property to the number of seconds to hang onto the data.

You should also consider using the OutputCache object for your pages. For example, consider whether you need to regenerate all your pages each time you request them. If not, adding an OutputCache to your page in source view allows you to generate your page once for the first user who requests it and then display that same page for every subsequent user. Assuming you create only a handful of versions of the page, and assuming that you control these versions in the data sent to the page, you can use the VaryByParam attribute to cache those different versions. This directive ensures that you keep copies of the page for 10 minutes for each of the different values of the CustomerText and CountryText controls:

<%@ OutputCache Duration="600" 
VaryByParam="CustomerText; CountryText" %>

ASP.NET 2.0 includes its own new wrinkles. Assume you have a page that contains a small part that you must regenerate for each request. You can use a Substitution control to define that part of the page. Simply set the control's MethodName property to the name of the function that provides the content for the control, and ASP.NET caches the rest of your page's HTML. This means your server has to generate much less HTML, improving server performance.

This might seem like a natural place to discuss ASP.NET 2.0's ability to discard cached data automatically if the data in the database changes. However, it remains unclear whether this feature will improve your performance if you don't also use SQL Server 2005. With the current versions of SQL Server, enabling dependencies between SQL Server and ASP.NET requires ASP.NET to check back with SQL Server on some schedule to see if data has changed. This extra work could degrade your performance more than caching helps it.

Caching also demonstrates what I said at the start of the article: Performance is often a trade-off between speed and memory. Faster processing often comes at the cost of using more memory or at the cost of changing your application's behavior.

The SqlDataSource object's DataSourceMode in ASP.NET 2.0 provides another demonstration of this rule. Setting the SqlDataSource's DataSourceMode to DataReader gives you the least memory consumption and fastest processing when retrieving data. However, controls bound to a SqlDataSource can lose some functionality when DataSourceMode is set to DataReader (the DataGrid won't be able to sort or filter data). Setting DataSourceMode to DataSet gives you that functionality, but at the cost of having the SQLDataSource take up more room in memory and take a little longer to retrieve your data. You might have to experiment to find what gives your site the best results.

Watch for Errors and COM
Things can go wrong, and you should provide error handling to catch those problems. But don't use error handling to take care of conditions that you can check for (unless those conditions are extremely rare). Exception handling is expensive in ASP.NET. You get better performance by checking for a condition and not raising the error than you do by putting the code in a Try block and hoping the condition doesn't exist. In other words, check whether your divisor is not equal to zero before dividing—don't attempt the division in a Try block.

If your application started life as an ASP page with VBScript, then you implemented your error handling using On Error Resume statements. ASP.NET supports using On Error Resume for Visual Basic—but it's extremely slow. Replace your On Error Resume statements with Try blocks.

If you call a COM object from your ASP.NET page, you need to do more than set your variables to nothing to release your COM objects. Setting a variable to nothing only makes the wrapper that .NET puts around your COM object available for garbage collection. It can be a long time before your wrapper gets garbage collected, and your COM object hangs around in memory until that happens. Use the ReleaseComObject method of the System.Runtime.InteropServices.Marshal object when you finish with an object, passing the reference to your COM object to release it. Your .NET wrapper still must wait for the garbage collector to collect it before you can get back the memory held by your COM object.

Unfortunately, there is a catch when using ReleaseComObject: Under some circumstances, you must call the method more than once. In fact, you must keep calling ReleaseComObject until the method returns 0 to ensure that your COM object is released. This code does the trick:

Dim ComObject As SomeCOMObject
Do Until System.Runtime.InteropServices. _
	Marshal.ReleaseComObject(ComObject) = 0
Loop

ASP.NET 2.0 provides a simpler solution: Call the Marshal object's FinalReleaseComObject method, passing your object like this:

System.Runtime.InteropServices.Marshal. _
	FinalReleaseComObject(ComObject)

Another important performance principle is to make sure that you get the fastest (and fewest) compiles. In ASP.NET 1.0, you should change your application's Configuration setting before you release it. Simply select Build | Configuration Manager and change your application's Configuration setting to Release. This gives you a more optimized build and skips generating any debug support.

Note that your application's code is never fully compiled when it leaves Visual Studio—at best, .NET converts your code to IL code, but the code must be compiled to native code after you install the application on the server. In ASP.NET 1.1, this final compilation happens the first time that a page is requested from the site. You shouldn't make your users wait for your site to compile; instead, you should force compilation by requesting a page from the site yourself.

The situation is compounded in ASP.NET 2.0 where two of the deployment methods (copying the Web site and creating a setup package) don't even compile your source code to IL. Only Build | Publish Web Site generates IL code. You can force compilation for your Web site by calling the precompile.axd utility after installing your application (the utility ships with .NET 2.0). All you need to do is append the utility's name to the end of your site's URL, like this: http://MyServer/MySite/precompile.axd.

I hope these tips prove useful to you. The usual disclaimer applies: Results are dependent on the application, and your mileage may vary. In any case, trying out some of these changes and monitoring your site's performance will give you the best insights for what works on your site. In addition, ASP.NET 2.0 will have different performance characteristics than ASP.NET 1.1. Remain up to date by constantly searching the MSDN site as well as Visual Studio Magazine's Web site for new articles with the keywords "ASP.NET" and "performance." Like life, performance is a journey, not a destination.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube