The performance of web applications is a serious concern for
application developers in Microsoft .NET. This article discusses some tips and
best practices that can be utilized for making performance improvement in .Net web
applications. It also focuses on the art of better coding practices that enable
reusability of code, ensure better code maintenance and improve performance of
the application as a whole.
Postbacks and DB Hits
Your code should be written to check for postbacks and
minimize database hits. This will reduce network traffic. Check for postback
judiciously to avoid unnecessary database hits. The following code shows how
to populate a control that contains relatively static data while avoiding
redundant database hits. Implement this code using Ajax UI and Client Side
scripts to avoid round trips to the server.
Listing 1
private void Page_Load(object sender,System.EventArgs e)
{
if(!IsPostBack)
BindData(ddListCountry,”Select * fromcustomer”,”id”,”name”);
}
private void BindData(DropDownListdropDownList,string sqlQuery,
string dataValueField,string dataTextField)
{
SqlConnection sqlConnection = newSqlConnection();
SqlCommand sqlCommand = new SqlCommand();
sqlConnection.ConnectionString =
ConfigurationSettings.AppSettings["CString"].ToString();
try
{
sqlConnection.Open();
sqlCommand.CommandText = sqlQuery;
sqlCommand.Connection = sqlConnection;
dropDownList.DataSource =sqlCommand.ExecuteReader();
dropDownList.DataValueField =dataValueField;
dropDownList.DataTextField =dataTextField;
dropDownList.DataBind();
}
catch(Exception ce)
{
//Usual code
}
finally
{
sqlConnection.Close();
}
}
Note that the BindData method is called within the
if(!IsPostBack) block, which checks to ensure that it is called only once. The
BindData method accepts an object of type DropDownList as the first parameter. The
second parameter is the SQL query string. The other two parameters are the
DataTextField and the DataValueField. Hence, this method can be re-used to
bind controls of the same type elsewhere. This ensures reusability.
String Operations
Do not compare strings using str ==”” because it creates an
additional empty string. Instead, use String.Empty for empty strings.
Test for empty strings using the Length property. This
works faster than using other methods. There are other alternatives like
String.Empty or the null string using “”, but I would suggest the Length
property as it is much faster. Refer to the following piece of code:
string s = “Joydip”;
if(s.Length != 0)
Response.Write(“The string is non blank”);
A string is an immutable set of characters. Whenever we
make any changes to it, we do not get the altered version of the original
string that we started with, but a new string object. The StringBuilder, on
the other hand, uses the same buffer. Therefore, always use the StringBuilder for
string concatenations rather than using strings.
Refer to the following piece of code:
Listing 2
using System;
using System.Text;
namespace ConsoleApplication
{
class Test
{
[STAThread]
static void Main(string[] args)
{
DateTime startTime =DateTime.Now;
String str = String.Empty;
Console.WriteLine("Thestart time is: "+startTime);
for(int i = 0;i<30000;i++)
str += "Joydip";
DateTime endTime =DateTime.Now;
Console.WriteLine("Theend time is: "+endTime);
startTime = DateTime.Now;
Console.WriteLine("Thestart time is: "+startTime);
StringBuilder sb = new StringBuilder();
for(int i = 0;i<30000;i++)
sb.Append("Joydip");
endTime = DateTime.Now;
Console.WriteLine("Theend time is: "+endTime);
}
}
}
I tested the above code and found that in the first case,
the loop executed in three seconds, while the second loop finished in less than
a second. In the second case, there are no extra string object allocations in
the heap, as in the first case using the string class.
When comparing strings, it is always advisable to use the
Compare method of the String class.
Refer to the following piece of code:
static bool Compare(string firstString, stringsecondString)
{
return (firstString.ToUpper() ==secondString.ToUpper());
}
In this piece of code, new strings are created due to the
call to the method ToUpper. Avoid extraneous string creation by calling the
overload of String.Compare which performs a case-insensitive comparison (when
its third parameter is set to true).
Data Access Techniques
DataReaders provide a fast and efficient method of data
retrieval. DataReader is much faster than DataSets as far as performance is
concerned. When databinding to controls it is advisable to use DataReaders for
faster data retrieval. Use DataReaders when you do not need to cache data. Use
DataSets (or simply DataTables) when you need to pass the data across different
layers.
When binding controls using a DataReader, use batched queries
if possible for better performance gains, rather than opening the connection
every time and querying the results. Refer to the following code:
Listing 3
private void Page_Load(object sender,System.EventArgs e)
{
if(!IsPostBack)
{
SqlConnection sqlConnection = newSqlConnection();
SqlCommand sqlCommand = newSqlCommand();
sqlConnection.ConnectionString =
ConfigurationSettings.AppSettings["CString"].ToString();
try
{
sqlConnection.Open();
sqlCommand.CommandText ="select * from customer;select *
from product";
sqlCommand.Connection =sqlConnection;
SqlDataReader sqlDataReader =sqlCommand.ExecuteReader();
ddListCustomer.DataSource =sqlDataReader;
ddListCustomer.DataValueField ="id";
ddListCustomer.DataTextField ="name";
ddListCustomer.DataBind();
sqlDataReader.NextResult();
ddListProduct.DataSource = sqlDataReader;
ddListProduct.DataValueField ="id";
ddListProduct.DataTextField ="name";
ddListProduct.DataBind();
}
catch(Exception e)
{
//Usual code
}
finally
{
sqlConnection.Close();
}
}
}
Set the EnforceConstraints property of the dataset to false
to turn off the constraints checking. Then use BeginLoadData and EndLoadData
methods to turn off the index maintenance for faster processing in a dataset
that needs to be populated from the database. Refer to the code that follows:
Listing 4
DataSet dataSet= new DataSet();
SqlConnection sqlConnection = new
SqlConnection(ConfigurationSettings.
AppSettings["ConnectionString"].ToString());
string sqlString="select * fromproduct";
sqlConnection.Open();
SqlDataAdapter da = new
SqlDataAdapter(sqlString,sqlConnection);
dataSet.Tables.Add("product");
dataSet.EnforceConstraints = false;
dataSet.Tables["product"].BeginLoadData();
da.Fill(dataSet.Tables["product"]);
dataSet.Tables["product"].EndLoadData();
//Some code to bind data to the controls
sqlConnection.Close();
When you require only one value from the database table, use
the ExecuteScalar method.
Page Rendering Issues
Avoid large sized pages to minimize network traffic. Use a
Custom Filter class to filter out the unwanted space characters in a page
rendered output. Use the Response.Filter property for this. Then use the following
code in the Global.asax file to filter the unwanted spaces or other characters
in the rendered output.
Listing 5
protected void Application_BeginRequest(Objectsender, EventArgs e)
{
Response.Filter = newCustomFilter(Response.Filter);
}
The CustomFilter class needs to be implemented to trim the
unwanted characters and spaces from the rendered output. An example of this
can be found here.
Note that many web servers implement compression, which does a great job of
reducing whitespace, so in many cases simply removing whitespace will provide
minimal gains. However, a filter for HTML comments or other unnecessary blocks
of content could reduce the size of the page enough to make it worthwhile.
Avoid late binding and initialization code for every postback
for a faster page rendering.
Stored Procedures
Stored Procedures do not have to be interpreted, compiled or
even transmitted from the client. This minimizes the network traffic and
server overhead. SQL Server creates an execution plan in the memory at the
time of executing a stored procedure. When the same procedure is executed next
time, the stored procedure executes faster since the plan is available from the
memory itself. On the other hand, if SQL queries are executed, the execution
plans are re-created each time (with some exceptions – frequent query plans are
cached). This makes it slower compared to the execution of a stored procedure.
When a stored procedure is executed, a message is
transmitted from the server to the client indicating the number of rows that
are affected. This information typically is not required and you can turn this
off to reduce the network traffic and improve the performance.
Include the following statement at the beginning of all
stored procedures to eliminate this message:
Exception Management
Use minimal exceptions within code, as they take a long time
to process and create a performance overhead. Do not use exceptions to control
flow of business logic. Do not write a try – catch block for something you can
easily test for using an if() statement. Write code that avoids exceptions. Refer
to the following piece of code.
Listing 6
int i = 5,j = 0,k;
try
{
k = i/j;
}
catch(Exception e)
{
// Some code
}
The above code can be written without using exceptions as:
Listing 7
int i = 5,j = 0,k = 0;
if(j!=0)
k = i/j;
Always try to use validation code to reduce exceptions in
your code, rather than using exceptions for controlling application flow.
Custom exception classes should not derive from the base class
SystemException (or Exception), but instead should inherit from
ApplicationException (or one of its subclasses). Do not have an empty catch
for an exception handler.
Do not clear the stack trace when re - throwing the
exceptions. Refer to the code that follows.
Listing 8
try
{
// Some code that raises an exception
}
catch(Exception e)
{
throw e;
}
In the above code, if you examine the stack trace, the point
of the exception would be the statement throw e. It is always advisable to
write the code as:
Listing 9
try
{
// Some code that raises an exception
}
catch(Exception e)
{
throw;
}
In the above case, we simply use the throw statement that
preserves the stack trace intact. It should be noted that re-throwing an
exception is expensive as far as performance is concerned. Use the finally
block to free unwanted resources. A connection that is opened in the try block
should always be closed in the finally block, as it is guaranteed to be
executed irrespective of whether an exception occurs or not. Alternately, wrap
resources like connections in using() statements to ensure they are properly
disposed.
Properties & Methods
Avoid methods with variable number of arguments, as it would
result in code paths for each possible combination of the parameters. In such
a case, use overloaded methods for better performance. Avoid using virtual
methods, as they result in slower performance due to the overhead involved in
the virtual table lookup. Obviously these recommendations cannot always be
followed, but should be considered whenever possible.
Code Optimization
Optimize code efficiently for better performance. Seal the
classes that do not need to be inherited. This will help in compiler
optimizations. You should then optimize assignments. Avoid using code like x
= x +1; it is always better to use x+=1. This is because, in the first case,
the JIT evaluates the variable x twice, which is not required. Avoid
unnecessary access to properties. Refer to the code below.
Listing 10
String[] arr = new string[] {“one”, “two”,“three”};
for(int x=0; x<arr.Length;x++)
{
//Some code
}
In the code above, the length property of the ArrayList
class is called every time the loop iterates. This repetitive access to the
property is unnecessary. The same code can be written as:
Listing 11
String[] arr = new string[] {“one”, “two”,“three”};
for(int x=0,len = arr.Length; x<len;x++)
{
//Some code
}
In the above case, the integer variable len stores the value
of the number of items in the arr object in the initializer portion of the
loop. Note that the initializer portion of a loop is executed only once. The
checking x<len in the condition evaluation portion of the loop ensures that
the repetitive access to the Length property is avoided.
Avoid the use of the volatile keyword. Accessing volatile
members is slower than non-volatile ones.
Avoid creating objects inside a loop. Refer to the code
below.
Listing 12
for(int i=0;i<10;i++)
{
DataSet ds = new DataSet();
// Some code
}
Each time the loop is encountered, the DataSet object ds
will be created.
Change the above code as:
Listing 12
DataSet ds = new DataSet();
for(int i=0;i<10;i++)
{
// Some code
}
Avoid usage of class variables unless absolutely necessary. Use
local variables as much as possible, as they remain in memory for a lesser
period of time and facilitate Garbage Collection. Consider the following code:
Listing 13
for(int p=0; p< 100;p++)
{
DisplayEmployeeSalary(p);
}
void DisplayEmployeeSalary(int empCode)
{
// Some Code
}
Imagine the number of calls made to the method
DisplayEmployeeSalary. Avoid putting such expensive operations in a loop. It
is better to inline the code by putting the entire method body in the loop
itself to avoid the calls made. In cases like this a tradeoff must sometimes
be made between well-factored code and optimally performing code, based on the
application’s needs.
Avoid using recursion in your code, as it uses the stack for
all the recursive calls made.
State Management
If the code does not need the usage of ViewState or
Sessions, turn them off. Both of these states should be used judiciously to
avoid performance degradation of the web applications.
ViewState is turned on by default. To disable ViewState, use
the following:
To disable ViewState for a single page:
<%@ Page EnableViewState="false" %>
In the web.config file to disable view state for an
application:
<pages enableViewState="false" />
In the machine.config file to disable ViewState for all
applications in the web server:
<pages enableViewState="false" />
Avoid storing too many objects in the session state. If the
session state is not used or there are pages that would in no way make any
changes to the objects stored in the session state, set the session state
appropriately as covered in this section.
To disable Session state for a specific page:
<%@ Page EnableSessionState="false" %>
Do not store too many objects in the Session state. If the
Session state is not required, turn it off. To set the Session state to read
only for a specific page:
<%@ Page EnableSessionState="ReadOnly"%>
To disable Session for the entire application, usethe following in the web.config file:
<sessionState mode='Off'/>
To disable Session state for all applications that run in
the web server, use the following in the machine.config:
<sessionState mode='Off'/>
Caching is a strategy that improves the performance of web
applications to a great extent. Cache the right data and use the right type of
caching for performance gains. Use page output or fragment caching depending on
the requirement for better performance. Use data caching for static data or
relatively static data access. Accessing data from the Cache is much faster
than accessing the same from the database. Learn more about caching from the ASPAlliance Caching Resource
Center.
Server.Transfer
Use Server.Transfer to redirect to another page within the
same application to avoid an unnecessary roundtrip to the client. Response.Redirect
sends a response header to the client that causes the client to send a new
request back to the server. If authentication and authorization checking is required,
using the Response.Redirect method becomes a necessity. In such a case, pass a
boolean value false to the method Response.Redirect to suppress the exception
that is raised.
Resource Management
Free or dispose of resources that are not needed. Database
connections and file resources should always be explicitly closed after use. Open
database connections in the try block and close the connection in the finally
block. Note that the finally block is executed irrespective of whether an
exception occurs or not.
Listing 14
//Some code
try
{
con.Open();
}
catch(Exception e)
{
//Some Code
}
finally
{
con.Close();
}
For objects that implement IDisposable, use the using()
block to accomplish the same thing. Also note that you may need to check for
null in your finally block if the objects you are cleaning up may not be
instantiated, perhaps due to an exception.
Locking and Shared Resources
Acquire shared resources late and dispose of them early. Avoid
locking and synchronization unless absolutely necessary. Do not lock on the
“this;” it is better to use a private object to lock on. Do not lock on the
type of the object.
Collections
Use the right type of collections and try to use strong
typed arrays over collections to prevent the boxing and un-boxing overhead. If
collections are required, analyze the requirements and select the right
collection type. Hence, you should judge which collections to choose depending
upon whether you require sorting the data or additional functionality such as
an index-based search, etc. When using Collections, initialize Collection
objects to the appropriate size. It is preferable to design strongly typed
collections to avoid the boxing and un-boxing overhead. In .NET 2.0, favor
Generic collections as they provide the benefits of strong typing with minimal
custom code.
Memory Management
The .NET runtime manages memory using a technique known as Garbage
Collection. Avoid making explicit calls to GC.Collect, as it tends to make a
full collection of all generations. Consider using Weak references with cached
data. Write code in such a way that the short-lived objects are not promoted
to higher generations. Always set the variables or objects that are no longer
needed to null before a long running call is made. Call the methods finalize
or Dispose on classes that implement them. Avoid implementing the finalize
method unless absolutely necessary. If you implement the Dispose method,
suppress the finalization in the Dispose method.
Further Reading
Please refer to the following links for more information on
this topic:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/scalenet.asp
Book: Improving
.NET Application Performance and Scalability
Conclusion
This article has highlighted many issues on the performance
aspects of web applications designed in .NET, such as writing efficient
structured code and avoiding common performance pitfalls. As this is meant to
be an overview, some recommendations do not have complete explanations; please
refer to the further reading resources above for more detail.