How to Maximize Performance and Scalability in ASP.NET Core
By FoxLearn 1/2/2025 3:43:04 AM 45
For high-traffic websites, such as Agoda, performance optimization can lead to significant cost savings by reducing the need for multiple app instances. During Agoda's transition from ASP.NET 4.7.2 to ASP.NET Core 3.1, the focus was on achieving this goal. Different businesses require varying levels of optimization, so a comprehensive list of server-side optimizations is provided, ranging from simple improvements to more complex micro-optimizations, helping to maximize performance and scalability.
1. Minimize Database and API Calls for Better Performance
To maximize performance and scalability, one key optimization is to reduce the number of database and external API calls your application makes. These calls are often slow and can significantly impact performance.
To address this, it's recommended to implement analytics or logging to monitor the speed of database and API calls. This helps assess the level of slowness and determine if the calls are necessary or can be minimized.
public async Task<IEnumerable<Product>> GetProductDetailsAsync(IEnumerable<int> productIds) { var products = new List<Product>(); foreach (var id in productIds) { var product = await _productRepository.GetProduct(id); products.Add(product); } return products; }
In this example, instead of retrieving people, the method retrieves product details by querying the _productRepository
for each product ID in the productIds
collection asynchronously.
To further reduce API and database call frequency, consider using caching. ASP.NET Core offers a simple caching solution with IMemoryCache, which is fast and easy to implement. However, there are some trade-offs: while data retrieval is quick, if you have multiple servers, cache misses may occur, and distributed caching would be a better choice. Additionally, since IMemoryCache uses the server’s RAM, it's important to be mindful of how much data is stored to avoid overloading the system.
2. Leverage Async Methods for Improved Performance
Using async/await does not automatically make your application faster, especially in low-traffic web apps where performance may slightly dip. The true benefit of async is scalability. In synchronous operations, each request is handled by a dedicated thread that waits during I/O operations, causing inefficiency. Async allows idle threads to return to the thread pool for reuse, improving server throughput and performance. This approach reduces the overhead of creating new threads and helps optimize the application, allowing for better scalability in high-traffic scenarios.
The async/await pattern allows tasks to start immediately, improving performance by reducing unnecessary delays. Instead of awaiting each async method as it’s called, you can launch multiple tasks concurrently and only await their results when needed. This introduces parallelism and boosts efficiency.
Imagine a scenario where you need to retrieve multiple user details from different sources. Here's a typical approach where tasks are awaited immediately:
var userDetails = await GetUserDetailsAsync(userId); var userPreferences = await GetUserPreferencesAsync(userId); var userOrders = await GetUserOrdersAsync(userId);
Instead, you can launch them concurrently, like so:
var userDetailsTask = GetUserDetailsAsync(userId); var userPreferencesTask = GetUserPreferencesAsync(userId); var userOrdersTask = GetUserOrdersAsync(userId); // Await only when the results are needed var userDetails = await userDetailsTask; var userPreferences = await userPreferencesTask; var userOrders = await userOrdersTask;
This small change enhances performance by running tasks in parallel, and the key takeaway is to only await when you truly need the result, not as soon as the task starts.
3. Proper Use of HttpClient for Optimal Performance
The HttpClient
class in .NET is often misused by creating and disposing of a new instance for each API call. This can lead to socket exhaustion under heavy load, severely impacting server throughput. A better practice is to reuse HttpClient
instances, and in .NET Core 2.2+, you can efficiently manage this with HttpClientFactory
, which handles connection reuse behind the scenes.
Instead of creating a new HttpClient
instance for each file download, like this:
using (var client = new HttpClient()) { var fileContent = await client.GetStringAsync("https://example.com/file"); }
A better approach is to reuse HttpClient
for multiple requests, as shown below:
public class FileDownloadService { private readonly HttpClient _httpClient; public FileDownloadService(IHttpClientFactory httpClientFactory) { _httpClient = httpClientFactory.CreateClient(); } public async Task<string> DownloadFileAsync() { var fileContent = await _httpClient.GetStringAsync("https://example.com/file"); return fileContent; } }
By injecting IHttpClientFactory
, the HttpClient
instance is reused, preventing socket exhaustion and improving server performance.
4. Leverage <Cache> Tag Helper in Razor Pages
Tag helpers, introduced in ASP.NET Core, are an improved version of Html Helpers. The <cache>
tag is one such helper that simplifies rendering and caching parts of a page to MemoryCache. These tag helpers offer many options, making them a useful tool for Razor Pages developers.
Instead of using traditional HTML helpers for form elements, you can use tag helpers in Razor Pages for a more convenient approach.
For example, using the <input>
tag helper:
<form method="post"> <input asp-for="UserName" /> <button type="submit">Submit</button> </form>
This tag helper automatically binds to the UserName
property, simplifying the markup and making it more readable. Similarly, the <cache>
tag helper helps with caching content, streamlining performance optimizations for Razor Pages. These helpers offer a range of useful features for enhancing your page development.
5. Use gRPC for Efficient Backend Service Calls
Switching from REST to gRPC can improve the performance of web applications that call various microservices. Developed by Google, gRPC leverages the HTTP/2 protocol and binary payloads for better connection management and reduced CPU overhead. HTTP/2 allows multiple calls over a single connection, improving server throughput, while the binary payload minimizes serialization/deserialization costs.
6. Reducing Memory Allocations
To further enhance web application performance, it’s crucial to optimize Garbage Collection (GC) by reducing the duration and frequency of interruptions. GC pauses can slow down operations as the CPU deals with cleaning up memory. By following principles like minimizing object creation, using smaller objects, and preallocating memory for collections, you can improve server performance. Additionally, using structs
over classes
and reusing collections like Lists
and Dictionaries
can further optimize memory management and reduce GC overhead.
When optimizing web application performance, another important factor is the management of memory and Garbage Collection. The goal is to reduce unnecessary memory allocations and minimize the frequency of GC pauses that can affect server throughput.
Preallocate Memory for Collections: If you use collections like List<T>
, the runtime dynamically resizes the underlying array, which can lead to inefficiencies. Instead of relying on the runtime to guess the size, you can preallocate the collection with a specified capacity:
List<int> numbers = new List<int>(1000); // Preallocate for 1000 items
Use Structs Over Classes (When Appropriate): In scenarios where many short-lived objects are created, switching from class
to struct
can reduce GC overhead. Structs
are value types, so they are allocated on the stack and don't require GC for cleanup.
Reuse Collections: Instead of creating new collections repeatedly, you can reuse existing ones by clearing their contents. This reduces memory allocation and avoids creating new arrays:
List<int> reusableList = new List<int>(); reusableList.Add(1); reusableList.Clear(); // Reuse the list without allocating new memory
By following these principles, you can help your application perform more efficiently and reduce the impact of Garbage Collection, leading to better overall server performance. These strategies were part of Agoda's optimization efforts when migrating to ASP.NET Core.
- How to use Brotli for response compression in ASP.NET Core
- How to use SignalR in ASP.NET Core
- How to use the Dapper ORM in ASP.NET Core
- How to enable CORS in ASP.NET Core
- How to implement HTTP.sys web server in ASP.NET Core
- How to use File Providers in ASP.NET Core
- How to use policy-based authorization in ASP.NET Core
- How to enforce SSL in ASP.NET Core