Slow webpages kill conversions and search rankings. This article is about common problems with ASP.NET site performance and how to optimize your site to overcome them.
There are many ways you can measure performance in applications. In this article I’m going to use an application performance monitoring (APM) tool to measure performance, identify problems, and get to the root cause. We will then provide multiple ways you can remediate and potentially avoid ASP.NET performance issues. We are using the SolarWinds® AppOptics™ for some of our examples.
Databases – The Source of Truth and (Often) Slowness
After setting up our APM tool and collecting a little data, you can begin measuring the performance of your queries to determine if they are a performance bottleneck.
In the dashboard, you can select the database tab and look at the “Total Time” column to find where you should start.
If nothing jumps out at you as being an unreasonable total time, you might be good to go. Think about all the work you just avoided. On the other hand, we can see that the “mongodb find hotel” query is taking up the majority of the total time.
You can click on the troublesome query to drill down into more specifics. Here we can see a list of the slowest queries in descending order.
Now, by clicking on the Trace button for a specific query, we can gain insight into the culprit code.
Generally, you will find two types of issues – slow queries and too many queries.
Slow Queries
Slow queries can be caused by many things. One common example is using non-parameterized queries.
Instead of using something like:
SqlCommand objCommand = new SqlCommand(“SELECT * FROM tblUser WHERE Name = ‘” + txtName + “‘ AND Password = ‘” + txtPassword + “‘”);
Use:
SqlCommand objCommand = new SqlCommand(“SELECT * FROM User WHERE Name = @Name AND Password = @Password”, objConnection);
objCommand.Parameters.Add(“@Name”, txtName);
objCommand.Parameters.Add(“@Password”, txtPassword);
Besides being faster due to only having to plan the query once, it is also much safer since it prevents SQL injection attacks. Twice as good for half the work.
Too Many Queries
Recently, I helped a client reduce a page’s load time from 20 seconds to two seconds, simply by reducing the number of queries required for the page to load.
Too many queries are often due to the well-known and dreaded N+1 problem. This is running a query, or more than one, for each result from a prior query. This problem is exceptionally easy to run into in ASP.NET due to the ItemDataBound and Repeater patterns. It’s easy to overlook the impact of something like this at a small scale, but it can wreak havoc on large-scale applications. It’s best to get in front of the problems before they arise.
For example, if the top-level query is:
SELECT * FROM Users
You might then do additional lookups in the ItemDataBound for the user’s favorite colors:
SELECT * FROM FavoriteColours WHERE UserID = @UserID
So now, for displaying a page with 25 users, you are doing 26 queries with latency on each call! You fix this by getting all the data in the initial query.
SELECT * FROM Users INNER JOIN FavoriteColours ON User.ID = FavoriteColours.UserID
If you are using LINQ, then a quick and dirty shortcut to increased performance is to compile your LINQ query. By merely wrapping your LINQ in a CompiledQuery.Compile you can get rid of all the LINQ overhead. Unfortunately, this doesn’t have support for all LINQ features (like .Contains), but it does support a surprising amount of LINQ you find in the real world.
Caches
Caches are often the backbone of modern website performance. However, it’s important to optimize your cache policies to increase the percentage of hits. Additionally, large cache objects can slow down responses just like slow database queries. The more you depend on cache as a core part of your application, the more vital it is that you monitor it. When going from development to production, you will often have an order of magnitude increase in the number of objects in the cache. This can degrade performance significantly if you aren’t properly utilizing caches.
Common caches, such as Memcached and Redis, are supported out of the box by APM, including AppOptics. By going to the Caches tab, you can immediately see the frequency, hit rate, and amount of time spent on cache operations.
Outside APIs
As
the devices we use continue to get faster, the bottlenecks caused by outside
API calls become more relevant. You may think the extra few milliseconds a
yield call or a LINQ statement takes is the core problem, when the larger
problem is an external call taking a full three seconds. An APM tool can help
surface the severity of these bottlenecks.
Due to the services existing outside of your control, there is no way to fix
the fundamental problem. So, you have to fix the symptoms of the problem for
your users and your application performance.
The first thing to decide is “should this call be happening in real time?”
Your calls to external services should not be in the critical path for a page
render. Events, such as sending an email and posting a text message, can be
queued and handled after page render.
The second thing to consider is “can this call be executed asynchronously?” to avoid blocking the page load. If it can, switching to asynchronous calls can yield significant performance benefits.
Finally, you have to consider if this is something you can simply do without on the page. If you can do without, you can use a CancellationToken to give up on it after it has been pending for longer than what is reasonable for the value it adds to your page.
Grab Bag of Cheap Wins
This is a grab bag of inexpensive and obvious wins, but sometimes you simply don’t think of them until they are in a list!
These are all “if you can” and not automatic recommendations.
- Turn off session state – avoids the cost of session creation for each user.
- Disable view state – avoids the cost of maintaining the state on the server side.
- Set debug=false in web.config – avoids the cost of debug symbols and hooks.
- Use client-side scripts for validations – avoids rounds trips and server costs.
- Use Page.ISPostBack – avoids expensive recalculations that are only needed on first page load.
- Use StringBuilder – avoids expensive creation of new immutable string objects.
- SqlDataReaderr instead of Dataset – avoids the adapter and bookkeeping costs.
- Server.Transfer instead of redirection, it avoids the round-trip to the client.
Conclusion
Trying to implement every possible performance improvement is an impossible and unnecessary task. Therefore, it is absolutely vital that you measure the performance of your application and let the metrics guide where you target optimizations. APM tools are powerful and can show you exactly where the performance bottlenecks are in your application, as well as the causes of those bottlenecks. Some causes include slow queries, too many queries, outside API calls, and more.
If you would like to learn more about the APM tool we used in this article, SolarWinds AppOptics, and get started with optimizing your ASP.NET applications, you can dive into the AppOptics .NET Knowledge Base and learn first-hand by trying AppOptics for free.
For .NET applications, SolarWinds AppOptics is a cost-effective APM solution and includes features such as distributed tracing and query tracking. The install on IIS is as easy as installing the .NET Agent and restarting IIS.