Developing High-Performance ASP.NET Applications

The following guidelines list specific techniques that you can use to avoid writing code that does not perform at acceptable levels.

  • Disable session state when you are not using it. Not all applications or pages require per-user session state, and you should disable it for any that do not.

    To disable session state for a page, set the EnableSessionState attribute in the @ Page directive to false. For example, <%@ Page EnableSessionState="false" %>.

    Note   If a page requires access to session variables but will not create or modify them, set the EnableSessionState attribute in the @ Page directive to ReadOnly.

    Session state can also be disabled for XML Web service methods. For more information, see XML Web Services Created Using ASP.NET and XML Web Service Clients.

    To disable session state for an application, set the mode attribute to off in the sessionstate configuration section in the application's Web.config file. For example, <sessionstate mode="off" />.

  • Choose your session-state provider carefully. ASP.NET provides three distinct ways to store session data for your application: in-process session state, out-of-process session state as a Windows service, and out-of-process session state in a SQL Server database. Each has it advantages, but in-process session state is by far the fastest solution. If you are storing only small amounts of volatile data in session state, it is recommended that you use the in-process provider. The out-of-process solutions are useful primarily if you scale your application across multiple processors or multiple computers, or where data cannot be lost if a server or process is restarted. For more information, see ASP.NET State Management.

  • Avoid unnecessary round trips to the server. While it is tempting to use the time- and code-saving features of the Web Forms page framework as much as you can, there are circumstances in which using ASP.NET server controls and postback event handling are inappropriate.

    Typically, you need to initiate round trips to the server only when your application is retrieving or storing data. Most data manipulations can take place on the client between these round trips. For example, validating user input from HTML forms can often take place on the client before that data is submitted to the server. In general, if you do not need to relay information to the server to be stored in a database, you should not write code that causes a round trip.

    If you develop custom server controls, consider having them render client-side code for browsers that support ECMAScript. By using server controls in this way, you can dramatically reduce the number of times information is unnecessarily sent to the Web server. For more information, see Developing ASP.NET Server Controls.

  • Use Page.IsPostBack to avoid performing unnecessary processing on a round trip. If you write code that handles server control postback processing, you will sometimes want other code to execute the first time the page is requested, rather than the code that executes when a user posts an HTML form contained in the page. Use the Page.IsPostBack property to conditionally execute code depending on whether the page is generated in response to a server control event. For example, the following code demonstrates how to create a database connection and command that binds data to a DataGrid Web server control if the page is requested for the first time.

    Sub Page_Load(sender As Object, e As EventArgs)
            ' Set up a connection and command here.
            If Not (Page.IsPostBack)
                Dim query As String = "select * from Authors where FirstName like '%JUSTIN%'"
                myCommand.Fill(ds, "Authors")
                myDataGrid.DataBind()
            End If
    End Sub
    
    [C#]
       void Page_Load(Object sender, EventArgs e) {
            // Set up a connection and command here.
            if (!Page.IsPostBack) {
                String query = "select * from Authors where FirstName like '%JUSTIN%'";
                myCommand.Fill(ds, "Authors");
                myDataGrid.DataBind();
            }
        }
    

    Since the Page_Load event executes on every request, this code checks whether the IsPostBack property is set to false. If so, the code executes. If the property is set to true, it does not.

    Note   If you did not run such a check, the behavior of a postback page would not change. The code for the Page_Load event executes before server control events execute, but only the result of the server control events would render to the outgoing page. If this check is not run, processing is still performed for the Page_Load event and any server control events on the page.

  • Use ASP.NET server controls in appropriate circumstances. Review your application code to make sure that your use of ASP.NET server controls is necessary. Even though they are extremely easy to use, server controls are not always the best choice to accomplish a task, since they use server resources. In many cases, a simple rendering or data-binding substitution will do. The following example demonstrates a situation in which using a server control is not the most efficient way to substitute values into the HTML sent to the client. Each method sends the path to an image to be displayed by the requesting browser, but using server controls is not the most expedient approach, since the Page_Load event requires a call to the server for processing. Instead, use render statements or data-binding expressions.

    <script language="VB" runat="server">
    
        Public imagePath As String
        Sub Page_Load(sender As Object, e As EventArgs)
            '...Retrieve data for imagePath here....
            DataBind()
        End Sub
    
    </script>
    
    <%--The span and img server controls are unecessary.--%>
    The path to the image is: <span innerhtml='<%# imagePath %>' runat="server"/><br>
    <img src='<%# imagePath %>' runat="server"/>
    
    <br><br>
    
    <%-- Use data binding to substitute literals instead.--%>
    The path to the image is: <%# imagePath %><br>
    <img src='<%# imagePath %>' />
    
    <br><br>
    
    <%-- Or use a simple rendering expression...--%>
    The path to the image is: <%= imagePath %><br>
    <img src='<%= imagePath %>' />
    [C#]
    <script language="C#" runat="server">
    
        public String imagePath;
        void Page_Load(Object sender, EventArgs e) {
            //...Retrieve data for imagePath here...
            DataBind();
        }
    
    </script>
    
    <%-- The span and img server controls are unecessary...--%>
    The path to the image is: <span innerhtml='<%# imagePath %>' runat="server"/><br>
    <img src='<%# imagePath %>' runat="server"/>
    
    <br><br>
    
    <%-- Use data binding to substitute literals instead...--%>
    The path to the image is: <%# imagePath %><br>
    <img src='<%# imagePath %>' />
    
    <br><br>
    
    <%-- Or use a simple rendering expression...--%>
    The path to the image is: <%= imagePath %><br>
    <img src='<%= imagePath %>' />
    

    There are many other cases in which rendering or data binding is more efficient than using server controls, even when you use server control templates. However, if you want to programmatically manipulate a server control's properties, handle server control events, or take advantage of view-state preservation, then a server control would be appropriate.

  • Save server control view state only when necessary. Automatic view-state management is a feature of server controls that enables them to repopulate their property values on a round trip (without your having to write any code). This feature does affect performance, however, since a server control's view state is passed to and from the server in a hidden form field. You should be aware of when view state helps you and when it hinders your page's performance. For example, if you are binding a server control to data on every round trip, the saved view state is replaced with new values that are obtained from the data-binding operation. In this case, disabling view state saves processing time.

    View state is enabled for all server controls by default. To disable it, set the EnableViewState property of the control to false, as in the following DataGrid server control example.

    <asp:datagrid EnableViewState="false" datasource="..." runat="server"/>
    

    You can also disable view state for an entire page by using the @ Page directive. This is useful when you do not post back to the server from a page:

    <%@ Page EnableViewState="false" %>
    

    Note   The EnableViewState attribute is also supported in the @ Control directive, which allows you to control whether view state is enabled for a user control.

    To analyze the amount of view state used by the server controls on your page, enable tracing for the page (by including a trace="true" attribute in the @ Page directive) and look at the Viewstate column of the Control Hierarchy table. For information about tracing and how to enable it, see ASP.NET Trace.

  • Leave buffering on unless you have a specific reason to turn it off. There is a significant performance cost for disabling buffering of Web Forms pages.

  • Do not rely on exceptions in your code. Since exceptions cause performance to suffer significantly, you should never use them as a way to control normal program flow. If it is possible to detect in code a condition that would cause an exception, do so. Do not catch the exception itself before you handle that condition. Common scenarios include checking for null, assigning a value to a String that will be parsed into a numeric value, or checking for specific values before applying math operations. The following example demonstrates code that could cause an exception and code that tests for a condition. Both produce the same result.

    // Consider changing this...
    try {
       result = 100 / num;
    }
    catch (Exception e) {
      result = 0;
    }
    
    // ...to this.
    if (num != 0)
       result = 100 / num;
    else
      result = 0;
    [Visual Basic]
    ' Consider changing this...
    Try
       result = 100 / num
    Catch (e As Exception)
      result = 0
    End Try
    
    // ...to this.
    If Not (num = 0)
       result = 100 / num
    Else
      result = 0
    End If
    
  • Use the common language runtime's garbage collector and automatic memory management appropriately. Be careful about allocating too much memory per request because the garbage collector will have to do more work more often. Also, do not have unnecessary pointers to objects, since they will keep the objects alive, and try to avoid having objects with Finalize methods, since they will entail more work at a later time. In particular, never free resources in a call to Finalize, since the resource could consume memory until the garbage collector runs it. This last problem often ruins performance in Web server environments, since it can be easy to exhaust the availability of a given resource while waiting for Finalize to run.

    For more information about the garbage collector and automatic memory management, see Automatic Memory Management.

  • If you have a large Web application, consider performing pre-batch compilation. Batch compilation is performed whenever a first request is made to a directory. If no page in the directory has been parsed and compiled, this feature will parse and compile all pages in the directory in chunks to provide better disk and memory usage. If this takes too long, a single page will be parsed and compiled quickly so that the request can be processed. This feature gives ASP.NET a performance benefit, since it compiles many pages into a single assembly. Accessing a page from an assembly that is already loaded is faster than loading a new assembly per page.

    The downside of batch compilation is that if the server receives many requests for pages that have not been compiled, performance can be poor while the Web server parses and compiles them. To solve this problem, you can perform pre-batch compilation. To do so, before your application goes live, simply request a page from it — it does not matter which page. Then, when users first access your site, the pages and their assembly will already be compiled.

    There is no easy mechanism to tell when batch compilation occurs. Wait until the CPU idles or no more compiler processes, such as csc.exe (the C# compiler) or vbc.exe (the Visual Basic compiler), are being launched.

    Also try to avoid changing assemblies in the \bin directories for your applications. Changing a page will cause a reparse and compilation of that single page, but replacing an assembly in a \bin directory will cause a whole new batch compile of that directory.

    On larger scale sites with many pages, it may be better to design your directory structure differently based on how often you plan to replace pages or assemblies. Pages that will be changed infrequently could be stored in the same directory and pre-batch compiled at specific times. Pages that change more frequently should be in their own directories (a few hundred pages at most in each) for fast compiles.

    A Web application may contain many subdirectories. Batch compilation occurs at the directory level, not the application level.

  • Recycle processes when running ASP.NET Web applications on Internet Information Services 5.0. By default, ASP.NET on IIS 5 will service requests to it on an out-of-process worker process. This feature has been tuned for fast throughput. On pages that do any significant amount of work, the throughput cost of running out-of-process is not great (approximately 10 percent). Because of the many features and advantages of running ASP.NET in an out-of-process worker process, it is recommended for production sites.

    Keep in mind that you should recycle processes periodically, for both stability and performance reasons. Over long periods of time, leaked resources and bugs may affect Web server throughput. You should balance this with recycling too often, however, since the cost of stopping the worker process, reloading pages and reobtaining resources and data may override the benefits of a recycle.

    ASP.NET Web applications running on the Windows Server 2003 family, which uses IIS 6.0, do not need to have the process model setting adjusted.

  • Adjust the number of threads per worker process for your application if necessary. The request architecture of ASP.NET tries to achieve a balance between the number of threads executing requests and available resources. Given an application that uses enough CPU power, the architecture will allow only as many concurrently executing requests as there is CPU power available for. This technique is known as thread gating. However, there are conditions in which the thread-gating algorithm does not work well. You can monitor thread gating in PerfMon by using the Pipeline Instance Count performance counter associated with the ASP.NET Applications performance object.

    When the page calls an external resource, such as database access or XML Web service requests, the page request generally stops, freeing the CPU. If a request is waiting to be processed and a thread is free in the thread pool, the waiting request will start to be processed. Unfortunately, this can sometimes lead to a high number concurrently processing requests, and many waiting threads in the process, on the Web server, which adversely affects performance. Typically if the gating factor is the response time from an external resource, having too many requests waiting for it does not help the throughput of the Web server.

    To mitigate this, you can manually set the limit on the number of threads in the process by changing the maxWorkerThreads ** and maxIOThreads attributes in the <processModel> node of the Machine.config configuration file.

    Note   Worker threads are for processing ASP.NET requests, while IO threads are used to service data from files, databases, or XML Web services.

    The values assigned to these attributes are the maximum number of each type of thread per CPU in the process. For a two-processor computer, the maximum number is twice the set value. For a four-processor computer, it is 4 times the set value. For computers with four or eight CPUs, it may be good to change the default anyway. The defaults are good for 1 or 2 processor machines, but having 100 or 200 threads in the process for computers with more processors may more detrimental than beneficial to performance.

    Note   Too many threads in a process tend to slow down the server because of extra context switches, causing the operating system to spend CPU cycles on maintaining threads rather than processing requests.

  • Use the HttpServerUtility.Transfer method to redirect between pages in the same application. Using this method in a page, with Server.Transfer syntax, avoids unnecessary client-side redirection.

  • Make all modules in the request pipeline as efficient as possible. All modules in the request pipeline will have a chance to be run on each request. Therefore, it is vital that the code triggered when a request enters and leaves each module executes quickly, especially in a code path where the module's feature is not used. Performing throughput tests with and without the modules and profiles is very useful in determining how fast these methods are executing.

  • Use early binding in Visual Basic .NET or JScript code. Historically, one of the reasons that developers enjoy working with Visual Basic, VBScript, and JScript is their so-called typeless nature. Variables need no explicit type declaration and can be created simply by using them. When assigning from one type to another, conversions are performed automatically. However, this convenience can cause your application's performance to suffer greatly.

    Visual Basic now supports type-safe programming through the use of the Option Strict compiler directive. For backward compatibility purposes, ASP.NET does not enable this option by default. For optimal performance, however, it is highly recommended that you enable this option in your pages. To enable Option Strict, include a Strict attribute in the @ Page directive or, for a user control, in the @ Control directive. The following example demonstrates how to set this attribute and makes four variable calls that show how using the attribute causes compiler errors.

    <%@ Page Language="VB" Strict="true" %>
    <%
    Dim B
    Dim C As String
    
    ' This will cause a compiler error.
    A = "Hello"
    ' This will cause a compiler error.
    B = "World"
    ' This will not cause a compiler error.
    C = "!!!!!!"
    ' But this will cause a compiler error.
    C = 0
    %>
    

    JScript .NET also supports typeless programming, although it offers no compiler directive to force early binding. A variable is late-bound if any of the following occur:

    • It is declared explicitly as an Object.
    • It is a field of a class with no type declaration.
    • It is a private function or method member with no explicit type declaration, and the type cannot be inferred from its use.

    The last distinction is tricky because the JScript .NET compiler will optimize if it can figure out the type based on how a variable is used. In the following example, the variable A is early-bound, but the variable B is late-bound.

    var A;
    var B;
    
    A = "Hello";
    B = "World";
    B = 0;
    

    For the best performance, assign a type to your JScript .NET variables when you declare them. For example, var A : String.

  • Port call-intensive COM components to managed code. The .NET Framework provides an easy way to interoperate with traditional COM components. The benefit is that you can start taking advantage of the new technology while preserving your existing investments. However, there are some circumstances in which the performance cost of keeping your old components makes it worthwhile to migrate your components to managed code. Every situation is unique, and the best way to decide whether you need to port a component is to run performance measurements on your Web site. It is recommended that you investigate porting to managed code any COM component that requires a high volume of calls in order to interact.

    In many cases, it is not possible to migrate legacy components to managed code, particularly when initially migrating your Web applications. In such circumstances, one of the biggest performance impediments is marshaling data from unmanaged to managed environments. Therefore, when interoperating, perform as many tasks on one side or the other and then make one large call rather than a series of smaller calls. For example, all strings in the common language runtime are in Unicode, so you should convert any strings to Unicode in your component before you make a call to managed code.

    Also, release any COM objects or native resources as soon as they have finished processing. This allows other request to use them and minimizes the performance issues associated with requiring the garbage collector to release them later.

  • Avoid single-threaded apartment (STA) COM components. By default, ASP.NET does not allow any STA COM components to run within a page. To run them, you must include the ASPCompat=true attribute in the @ Page directive in the .aspx file. This switches the thread pool used for the execution to an STA thread pool, while also making the HttpContext and other built-in objects available to the COM object. The former is also a performance optimization, because it avoids any call marshaling from multithreaded apartment (MTA) to STA threads.

    Using an STA COM component can be a significant performance hit and should be avoided if possible. If you must use an STA COM component, as in any interop scenario, avoid making numerous calls during an execution and try to send as much information as possible during each call. Also, be careful to not to create any STA COM components during the construction of the page. For example, in the following code the MySTAComponent would be instantiated at page construction time, which is created from a thread that is not the STA thread that will run the page. This can have an adverse performance impact, since marshaling between MTA and STA threads will have to be done to construct the page.

    <%@ Page Language="VB" ASPCompat="true" %>
    <script runat=server>
      Dim myComp as new MySTAComponent()
    Public Sub Page_Load()
        myComp.Name = "Bob"
     End Sub  
    </script>
    <html>
    <%
        Response.Write(Server.HtmlEncode(myComp.SayHello))
    %>
    </html>
    

    The preferred mechanism is to delay the object creation until later when the code is executing under an STA thread, as in the following example.

    <%@ Page Language="VB" ASPCompat="true" %>
    <script runat=server>
      Dim myComp
    Public Sub Page_Load()
        myComp = new MySTAComponent()
        myComp.Name = "Bob"
     End Sub  
    </script>
    <html>
    <%
        Response.Write(Server.HtmlEncode(myComp.SayHello))
    %>
    </html>
    

    The recommended practice is to construct any COM component and external resources when needed or in the Page_Load method.

    You should never store any STA COM component in a shared resource where it can be accessed by threads other than the one that constructed it. This includes the cache and session state, for example. Even if an STA thread makes a call to a STA COM component, only the thread that constructed the STA COM component can actually service the call, which entails marshaling the call to the creator thread. This marshaling can have significant performance penalties and scalability problems. In such cases, explore the possibility of making the COM component into an MTA COM component, or better, porting the code to make the object a managed one.

  • Use SQL Server stored procedures for data access. Of all the data access methods provided by the .NET Framework, SQL Server–based data access is the recommended choice for building high-performance, scalable Web applications. When using the managed SQL Server provider, you can get an additional performance boost by using compiled stored procedures instead of ad hoc queries. For information about using SQL Server stored procedures, see Using Stored Procedures with a Command.

  • Use the SqlDataReader class for a fast forward-only data cursor. The SqlDataReader class provides a means to read a forward-only data stream retrieved from a SQL Server database. If situations arise while you are creating an ASP.NET application that allow you to use it, the SqlDataReader class offers higher performance than the DataSet class. This is the case because SqlDataReader uses SQL Server's native network data-transfer format to read data directly from a database connection. Also, the SqlDataReader class implements the IEnumerable interface, which allows you to bind data to server controls as well. For more information, see the SqlDataReader Class. For information about how ASP.NET accesses data, see Accessing Data with ASP.NET.

  • Choose the data viewing mechanism appropriate for your page or application. Depending on how you choose to display data in a Web Forms page, there are often significant tradeoffs between convenience and performance. For example, the DataGrid Web server control can be a quick and easy way to display data, but it is frequently the most expensive in terms of performance. Rendering the data yourself by generating the appropriate HTML may work in some simple cases, but customization and browser targeting can quickly offset the extra work involved. A Repeater Web server control is a compromise between convenience and performance. It is efficient, customizable, and programmable.

  • Cache data and page output whenever possible. ASP.NET provides simple mechanisms for caching page output or data when they do not need to be computed dynamically for every page request. In addition, designing pages and data requests to be cached, particularly in areas of your site where you expect heavy traffic, can optimize the performance of those pages. More than any feature of the .NET Framework, using the cache appropriately can affect the performance of your site, sometimes by more than an order of magnitude.

    There are two caveats to using the ASP.NET caching mechanisms. First, do not cache too many items. There is a cost for caching each item, especially in memory utilization. Items that are easily recalculated or rarely used should not be cached. Second, do not assign cached items a short expiration. Items that expire quickly cause unnecessary turnover in the cache and frequently cause more work for cleanup code and the garbage collector. If this is a concern, monitor the Cache Total Turnover Rate performance counter associated with the ASP.NET Applications performance object. A high turnover rate could indicate a problem, especially when items are removed before their expiration. This is also known as memory pressure.

    For information about how to cache page output and data requests, see ASP.NET Caching Features.

  • For applications that rely extensively on external resources, consider enabling Web gardening on multiprocessor computers. The ASP.NET process model helps enable scalability on multiprocessor computers by distributing work to several processes, one per CPU, each with processor affinity set to its CPU. This technique is called Web gardening. If your application uses a slow database server or calls COM objects that have external dependencies, to name only a couple of possibilities, it can be beneficial to enable Web gardening for your application. However, you should test how well your application performs in a Web garden before your decide to enable this.

  • Be sure to disable debug mode. Always remember to disable debug mode before deploying a production application or conducting any performance measurements. If debug mode is enabled, the performance of your application can suffer a great deal. For syntax information about setting the debug mode for your application in the Web.config file, see ASP.NET Settings Schema.

  • Tune the configuration files for your Web server computer and specific applications to suit your specific needs. By default the ASP.NET configuration is set to enable the widest set of features and try to accommodate most common scenarios. As such, some of these can be tuned and changed depending on the features used by the app developer to improve the performance of applications. The following list are some of the options you should consider.

    • Enable authentication only for those applications that need it. By default the authentication mode is Windows, or integrated NTLM. In most cases it is best to disable authentication in the Machine.config file and enable it in the Web.config files for your applications that need it.

    • Configure your application to the appropriate request and response encoding settings. The ASP.NET default encoding is UTF-8. If your application is strictly ASCII, configure your application for ASCII for a slight performance improvement.

    • Consider disabling AutoEventWireup for your application. Setting the AutoEventWireup attribute to false in the Machine.config file means that the page will not match method names to events and hook them up (for example, Page_Load). If page developers want to use these events, they will need to override the methods in the base class (for example, they will need to override Page.OnLoad for the page load event instead of using a Page_Load method). If you disable AutoEventWireup, your pages will get a slight performance boost by leaving the event wiring to the page author instead of performing it automatically.

    • Remove unused modules from the request-processing pipeline. By default, all features are left active in the <httpModules> node in your server computer's Machine.config file. Depending on which features your application uses, you may be able to remove unused modules from the request pipeline to get a small performance boost. Review each module and its functionality and customize it to your needs.

      For example, if you do not use session state and output caching in your application, you can remove each from the <httpModules> list so that requests do not have to perform the enter and leave code on each of these modules without performing any other meaningful processing.

See Also

ASP.NET Optimization | System Monitoring Components