Top Performance Tuning Tips for ASP.NET Core Applications
Add Listing

headerbtn

Top Performance Tuning Tips for ASP.NET Core Applications

ASP.NET Core applications must be fast, responsive, and scalable to meet the growing demands of modern digital experiences. To achieve these goals, .NET developers often implement a variety of performance optimization tactics.

Techniques like reducing response times, enhancing server load management, and streamlining database queries play a significant role in improving the app’s efficiency. Apart from these, minimizing unnecessary memory allocations, using asynchronous programming for I/O-bound tasks, and optimizing database queries with Entity Framework Core also come in handy. For businesses aiming to stay competitive, it is beneficial to hire .NET developers who are well aware of these strategies.

This blog highlights the practical, actionable tips to help .NET developers optimize their ASP.NET Core applications for peak performance.

Understanding ASP.NET Core Performance

ASP.NET Core is a powerful, open-source framework for building web applications. The performance of an ASP.NET Core application depends on various factors, such as the way requests are handled, the server setup, and the application’s internal structure. To gain a better understanding of ASP.NET Core performance, let’s explore some key components that influence it:

Request Handling and Middleware:

When a user makes a request to an ASP.NET Core application, it goes through a pipeline of middleware. Middleware is a component that can inspect, modify, or short-circuit requests and responses. The order in which middleware is executed affects the overall app performance. For instance, adding unnecessary or complex middleware can slow down response times.

Kestrel Web Server:

Kestrel is the web server used by ASP.NET Core. It’s designed to be lightweight and fast. However, performance can be affected by how it is configured and whether it’s being used with other web servers like IIS or Nginx in a reverse proxy setup. Proper configuration and load balancing are necessary to ensure high performance.

Request-Response Cycle:

ASP.NET Core’s efficient request-response cycle facilitates quick processing, but performance bottlenecks can occur in areas like database access, static file serving, or complex business logic. .NET developers need to optimize each step of the process to reduce delays and increase efficiency.

How to Improve ASP.NET Core Performance?

While ASP.NET Core is part of the broader .NET Core ecosystem, it is specifically optimized for developing fast and scalable web applications and services. Despite this fact, performance issues in ASP.NET Core are still present.

By following the ASP.NET Core performance tuning techniques below, you can minimize bottlenecks, decrease response times, and ensure your application is built for high-traffic loads:

1. Optimize Startup Performance

The startup time of an application is a critical factor in overall performance. A slow startup not only delays response times but can also increase server load. Here’s how you can speed things up:

  1. Minimize Startup Tasks: ASP.NET Core executes a lot of work during the startup process, including setting up services, middleware, and configuration. You can reduce unnecessary tasks by following the below tactics:
  2. Dependency Injection: Don’t inject unnecessary services into the ConfigureServices method. It’s recommended to only add what you absolutely need. Use scoped and transient services properly and avoid injecting complex objects that are created on every startup.
  3. Asynchronous Startup: Whenever possible, use asynchronous methods during startup. Use IApplicationBuilder.Use(async (context, next) => {…}) to offload long-running tasks to background threads, which improves the speed of handling requests.
  4. Precompile Views: In production environments, precompiling Razor views can greatly reduce startup time. By default, ASP.NET Core compiles Razor views at runtime, which can slow down the initial request.

You can precompile Razor views using the dotnet publish command:

dotnet publish --configuration Release --output ./publish

This process will compile all Razor views into assemblies, which speeds up the startup significantly.

2. Efficient Memory Management

If your app’s memory usage is high, chances are higher for the application to crash under heavy traffic. Hence, you should manage the memory of your ASP.NET Core application. But how is it possible? Let’s have a look:

  1. Avoid Excessive Memory Allocation: Every time you create a new object, the .NET runtime has to allocate memory for it, which can lead to garbage collection (GC) cycles. In fact, excessive memory allocation leads to higher GC overhead.
  2. Use stack-based structures like Span<T> and Memory<T> when working with small data sets, as they don’t require heap allocation and are faster than arrays or lists.
Span<int> numbers = stackalloc int[100];  // No heap allocation
  • Reduce Object Creation: If you create any unnecessary objects, the load on the garbage collector will increase. A good approach is to avoid allocating memory in tight loops or high-frequency methods. You can use object pooling to reuse objects instead of creating new ones every time.

ASP.NET Core provides built-in support for object pooling. For example, use ArrayPool<T> to reuse arrays:

ArrayPool<int> pool = ArrayPool<int>.Shared;

int[] buffer = pool.Rent(1024);  // Reuse array buffer

3. Caching Strategies

Caching can reduce the load on your database and improve response times by serving frequently requested data from memory. There are two types of caching:

  1. In-Memory Caching: In-memory caching is a simple, yet effective, method of caching. Use it for small datasets that are frequently accessed. ASP.NET Core’s built-in caching system (IMemoryCache) is a great choice for this purpose.
public class MyService

{

    private readonly IMemoryCache _cache;

    public MyService(IMemoryCache cache)

    {

        _cache = cache;

    }

    public string GetData()

    {

        return _cache.GetOrCreate("myKey", entry =>

        {

            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);

            return "cached data";

        });

    }

}

  • Distributed Caching: For applications that need to scale horizontally (multiple servers), using a distributed cache like Redis is essential. Redis can store cached data outside the app’s memory, making it accessible across different instances.

Here’s how you can set up a distributed cache:

public class MyService

{

    private readonly IDistributedCache _cache;

    public MyService(IDistributedCache cache)

    {

        _cache = cache;

    }

    public async Task<string> GetDataAsync()

    {

        string cachedData = await _cache.GetStringAsync("myKey");

        if (cachedData == null)

        {

            cachedData = "fresh data";

            await _cache.SetStringAsync("myKey", cachedData);

        }

        return cachedData;

    }

}

4. Optimize Database Calls

Many times, database queries can become bottlenecks. You need to optimize your database interaction to enhance your application performance. Here’s how you can do it:

  1. Efficient Database Access with Entity Framework: Entity Framework (EF) Core is a powerful ORM, but it’s important to use it efficiently to prevent unnecessary queries and overhead.
  2. AsNoTracking(): Use AsNoTracking() for read-only operations. This method tells EF Core not to track the entity, which speeds up the query execution.
var products = context.Products.AsNoTracking().ToList();
  • Optimize Queries: Avoid fetching unnecessary data. Use Select() only to retrieve the columns you need. This will reduce the amount of data pulled from the database.
  • Connection Pooling: ASP.NET Core automatically manages connection pooling, but configuring your database and connection strings properly can prevent excessive connections and disconnections.

In your appsettings.json:

"ConnectionStrings": {

  "DefaultConnection": "Server=myserver;Database=mydb;User Id=myuser;Password=mypassword;Max Pool Size=200;"

}

The Max Pool Size parameter helps you control how many connections are pooled at once, which can significantly improve performance when your application is under load.

5. Minimize HTTP Requests

Every HTTP request adds overhead to your application. Hence, reducing the number of HTTP requests your app makes can have a substantial impact on performance. You can decrease HTTP requests with these strategies:

  1. Enable Compression: By enabling response compression, you can significantly reduce the size of data transferred between the server and the client. Both Gzip and Brotli are excellent choices for compressing HTTP responses.

In your Startup.cs file:

public void Configure(IApplicationBuilder app)

{

    app.UseResponseCompression();

}

  • HTTP/2 and Static File Serving: Using HTTP/2 allows for multiplexing multiple requests over a single connection. A major benefit of this technique is that it reduces latency and improves the speed of responses.

For static files like images, scripts, and stylesheets, leverage CDNs to offload the delivery, reducing the load on your server.

6. Optimize Middleware

Middleware is integral to every ASP.NET Core Application. If the middleware pipeline is misconfigured, it will add unnecessary overhead. Optimizing middleware isn’t a big deal with these techniques:

  1. Avoid Heavy Middleware: Some middleware (such as logging and exception handling) can be resource-intensive. Always ensure that these are placed strategically in the pipeline to minimize their impact on request performance.
  2. Order of Middleware: ASP.NET Core processes middleware in the order in which it is registered. For performance reasons, place lightweight middleware like CORS and StaticFiles early and heavier ones (such as Authentication) later in the pipeline.

7. Asynchronous and Parallel Programming

When building high-performance applications, asynchronous and parallel programming are essential tools. They allow you to handle I/O-bound and CPU-bound tasks more efficiently, improving response times and throughput. Let’s look at how you can leverage both approaches in your ASP.NET Core application:

  1. Async I/O: Whenever you’re performing I/O operations, such as reading from a database or accessing a file, ensure you’re using async and await to free up the thread for other requests.
public async Task<IActionResult> GetData()

{

    var data = await _myService.GetDataAsync();

    return Ok(data);

}

  • Parallel CPU-bound Tasks: When you have CPU-intensive tasks, like processing large datasets or performing computations, parallelism can help split the workload across multiple threads, improving execution speed and reducing overall processing time.

For example, let’s say you’re processing a large collection of data where each operation is independent. You can use Parallel.ForEach() or Task.WhenAll() to perform those tasks concurrently.

Using Parallel.ForEach():

public IActionResult ProcessLargeDataset()

{

    var data = GetLargeDataset(); // Imagine this is a collection of items.

    // Process the items in parallel to utilize multiple CPU cores

 Parallel.ForEach(data, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, item =>

    {

        ProcessItem(item); // Your CPU-bound task, like complex calculations.

    });

    return Ok("Data processed in parallel!");

}

Here, Parallel.ForEach() divides the data into chunks and processes each chunk in parallel, which can significantly speed up the operation, especially when running on multi-core machines.

Using Task.WhenAll():

public async Task<IActionResult> ProcessLargeDatasetAsync()

{

    var data = GetLargeDataset(); // Collection of items to process

    var tasks = data.Select(item => Task.Run(() => ProcessItem(item))).ToArray();

    // Wait for all tasks to complete

    await Task.WhenAll(tasks);

    return Ok("Data processed in parallel asynchronously!");

}

In this instance, Task.WhenAll() is used with Task.Run() to create tasks for each item. Each task runs in parallel, processing different items simultaneously. The WhenAll() method ensures that all tasks are completed before continuing the execution flow.

Profiling and Monitoring Performance

When optimizing ASP.NET Core applications, you can’t afford to bypass profiling and monitoring. Let’s break down some key techniques to help you stay on top of profiling and monitoring performance:

1. Using .NET Core’s Built-in Profiling Tools

.NET Core provides various built-in tools for profiling performance. The Diagnostic Tools in Visual Studio let you monitor CPU usage, allocate memory, and even track down unoptimized code paths. The dotnet-counters tool is another useful one, allowing real-time monitoring of application performance metrics such as request rates, exceptions, and more. Here’s a quick command to get started:

dotnet-counters monitor --process-id <your-process-id>

2. Application Insights for Deep Monitoring

For a more robust solution, Application Insights is a must. It automatically collects telemetry data from your application, offering real-time insights into performance, failures, and dependencies. You can easily drill into specific requests and see which part of your code is slowing down the system.

3. Third-Party Performance Tools

Integrating third-party tools like New Relic or Datadog can take your monitoring to the next level. These tools offer in-depth performance analytics, allowing you to spot issues even before they become noticeable in production.

Advanced ASP.NET Core Optimization Tips

When it comes to fine-tuning the performance of your ASP.NET Core application, there are a few advanced techniques you can use to push your application even further. These strategies aren’t always necessary for every project, but they can make a big difference in high-performance environments:

1. Tiered Compilation in .NET Core

Tiered compilation is a powerful feature introduced in .NET Core. It speeds up your app’s startup time by compiling critical code paths more quickly while allowing less important methods to be compiled later. This improves both the startup and runtime performance of your application. Enabling this is as simple as setting it in the runtime configuration.

Example:

{

  "System.GC.TunedGC": "true",

  "TieredCompilation": "true"

}

2. JIT (Just-In-Time) and AOT (Ahead-Of-Time) Compilation

In JIT compilation, the code is compiled at runtime, just before it is executed. This allows the application to take advantage of the runtime environment’s capabilities, optimizing performance based on actual conditions.

Example:

Let’s say you have a simple method that calculates the factorial of a number:

public int Factorial(int n)

{

    if (n == 0) return 1;

    return n * Factorial(n - 1);

}

On the other hand, AOT compilation compiles the entire code of your application before it is deployed, meaning the code doesn’t need to be compiled at runtime. This can significantly improve startup time and reduce runtime overhead. It’s particularly beneficial for scenarios like microservices or applications running in resource-constrained environments (e.g., mobile apps).

Example:

In ASP.NET Core, you can use dotnet publish with the –configuration Release –self-contained flag to enable AOT compilation for a self-contained application. This ensures that the application is fully compiled before it runs.

dotnet publish -c Release --self-contained

3. Inlining Critical Code Paths

Inlining is when the compiler directly places the code of a method in the calling method. Besides minimizing overhead, it can lead to significant performance boosts in CPU-bound applications. You can instruct the compiler to inline critical methods using the MethodImplOptions.AggressiveInlining attribute as mentioned below:

[MethodImpl(MethodImplOptions.AggressiveInlining)]

public void ProcessData() { /* code */ }

Conclusion

By applying these ASP.NET Core optimization tips, like improving startup times, managing memory better, using caching strategies, and optimizing database queries, you’ll see significant improvements in your app’s responsiveness. Always remember performance tuning is an ongoing process. Regular profiling and monitoring are key to catching bottlenecks early. Keep experimenting, testing, and stay updated on the latest improvements in ASP.NET Core to keep your apps ahead of the curve.

About Author

Prev Post
Scaling Your Tech Team: Outsourcing .NET Development for Growth