Given that network bandwidth is a finite resource, optimizing its usage can markedly improve your application's performance. One effective strategy for maximizing network bandwidth utilization is response compression. It involves not only reducing the size of data transmitted from the server to the client, but can greatly improve the responsiveness of an application.
Configuration
Enabling response compression in ASP.NET requires you to do two things:
- Use
AddResponseCompression
to add the response compression service to the service container. - Use
UseResponseCompression
to enable response compression middleware in the request processing pipeline.
❗️ Important
Response compression middleware must be registered before other middleware that might write into the response.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCompression();
var app = builder.Build();
app.UseResponseCompression();
app.UseHttpsRedirection();
app.MapGet("/", () => "Hello World!");
app.Run();
Compression with HTTPS
Due to inherent security risks, response compression for HTTPS connections is disabled by default. However, if you need to use this feature, it can be enabled by setting the EnableForHttps
option to true
.
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
});
Using response compression over HTTPS can expose you to CRIME and BREACH attacks. These attacks can be mitigated in ASP.NET with antiforgery tokens.
The following is an example of a minimal API code that uses response compression with HTTPS enabled and implements antiforgery token checking:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseResponseCompression();
app.MapGet("/generate-token", (IAntiforgery antiforgery, HttpContext httpContext) =>
{
var tokens = antiforgery.GetAndStoreTokens(httpContext);
return Task.FromResult(tokens.RequestToken);
});
app.MapPost("/", async (IAntiforgery antiforgery, HttpContext httpContext) =>
{
try
{
await antiforgery.ValidateRequestAsync(httpContext);
return Results.Ok("Hello World");
}
catch
{
return Results.BadRequest("Invalid CSRF token");
}
});
app.Run();
In this code, when you make a GET request to /generate-token
, the server generates an antiforgery token and returns it. When you make a POST request to /
, you should include this token in the request header with the name X-CSRF-TOKEN
. The Antiforgery middleware will then validate the token. If the validation is successful, the response will be Hello World
. Otherwise, the response will be Invalid CSRF token
.
Providers
A compression provider is a component that implements a specific compression algorithm. It is used to compress data before it is sent to the client.
When you invoke the AddResponseCompression
method, two compression providers are included by default:
-
BrotliCompressionProvider
, using the Brotli algorithm. -
GzipCompressionProvider
, using the Gzip algorithm.
Brotli is the default compression setting if the client supports this compressed data format. However, if the client does not support Brotli but does support Gzip compression, then Gzip becomes the default compression method.
Also, it is important to note that when you add a specific compression provider, other providers will not be included automatically. As an instance, if you have only explicitly included the Gzip compression provider, the system will not add any other compression providers.
Here is an example where you enable response compression for HTTPS requests and add the response compression providers Brotli and Gzip:
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
You can set the compression level with BrotliCompressionProviderOptions
and GzipCompressionProviderOptions
. By default, both Brotli and Gzip compression providers use the fastest compression level, known as CompressionLevel.Fastest
. This may not result in the most efficient compression. If you are aiming for the best compression, you should adjust the response compression middleware settings for optimal compression.
Compression Level | Value | Description |
---|---|---|
Optimal | 0 | The compression operation should optimally balance compression speed and output size. |
Fastest | 1 | The compression operation should complete as quickly as possible, even if the resulting file is not optimally compressed. |
NoCompression | 2 | No compression should be performed on the file. |
SmallestSize | 3 | The compression operation should create output as small as possible, even if the operation takes a longer time to complete. |
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.SmallestSize;
});
Custom providers
A custom compression provider is a class that inherits from the ICompressionProvider
interface to provide a custom compression method for HTTP responses.
Unlike the other examples, this time I decided to create a real provider that you can reuse in your applications. The provider I am going to make is DeflateCompressionProvider
, which takes advantage of the Deflate algorithm.
First, I define the options that I will use in the provider. I implement within it the Level
property, specifying which compression level to use for the stream. The default setting is Fastest
.
public class DeflateCompressionProviderOptions : IOptions<DeflateCompressionProviderOptions>
{
public CompressionLevel Level { get; set; } = CompressionLevel.Fastest;
DeflateCompressionProviderOptions IOptions<DeflateCompressionProviderOptions>.Value => this;
}
Next, I create the compression provider using the built-in DeflateStream
class as the compression algorithm, specifying in its constructor:
- the stream to be compressed;
- one of the values in the
Level
enumeration property of options; -
true
to leave the stream object open after disposing theDeflateStream
object.
In addition, I specify in the EncodingName
property the encoding name that will be used in the Accept-Encoding
request header and the Content-Encoding
response header. I also set the SupportsFlush
property to true
with which I go to indicate whether the specified provider supports Flush
and FlushAsync
.
public class DeflateCompressionProvider : ICompressionProvider
{
public DeflateCompressionProvider(IOptions<DeflateCompressionProviderOptions> options)
{
ArgumentNullException.ThrowIfNull(nameof(options));
Options = options.Value;
}
private DeflateCompressionProviderOptions Options { get; }
public string EncodingName => "deflate";
public bool SupportsFlush => true;
public Stream CreateStream(Stream outputStream)
=> new DeflateStream(outputStream, Options.Level, leaveOpen: true);
}
Finally, I add the newly created compression provider into the application when I configure response compression:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<DeflateCompressionProvider>();
});
builder.Services.Configure<DeflateCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseResponseCompression();
app.MapGet("/", () => "Hello World!");
app.Run();
MIME types
The middleware for response compression provides a default collection of MIME types that can be compressed. For a comprehensive list of supported MIME types, refer to the source code.
You can modify or supplement the MIME types by using ResponseCompressionOptions.MimeTypes
.
⚠️ Warning
Wildcard MIME types, such astext/*
, are not supported.
In the following example, a MIME type is added for image/svg+xml
in order to compress .svg files:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml" });
});
var app = builder.Build();
app.UseResponseCompression();
Benchmark
For a more in-depth understanding of how much bandwidth can be saved, let's use the following minimal API:
app.MapGet("/", () => Results.Ok(
Enumerable.Range(1, 500).Select(num => new
{
Id = num,
Description = $"Hello World #{num}",
DayOfWeek = (DayOfWeek)Random.Shared.Next(0, 6)
})));
Using the different compression providers listed above, including the one I made myself, and the different compression levels used, we examine the results obtained:
Provider | Compression level | Response size | Percentage decrease |
---|---|---|---|
None | None | 28,93 KB | baseline |
Gzip | NoCompression | 29,00 KB | -0.2% (increase) |
Gzip | Fastest | 3,60 KB | 87,6% |
Gzip | Optimal | 3,32 KB | 88,5% |
Gzip | SmallestSize | 3,06 KB | 89,4% |
Brotli | NoCompression | 4,14 KB | 85,7% |
Brotli | Fastest | 3,29 KB | 88,6% |
Brotli | Optimal | 1,74 KB | 94,0% |
Brotli | SmallestSize | 1,74 KB | 94,0% |
Deflate | NoCompression | 28,99 KB | -0.2% (increase) |
Deflate | Fastest | 3,57 KB | 87,7% |
Deflate | Optimal | 3,29 KB | 88,6% |
Deflate | SmallestSize | 3,04 KB | 89,5% |
🔎 Insight
The formula for calculating the percentage decrease is as follows:Percentage decrease = ((Original size - Compressed size) / Original size) * 100
The first entry, None
, indicates the absence of a compression provider. It provides a baseline for understanding the effect of using different compression providers.
Next, the compression provider Gzip
is examined. When it is used at the NoCompression
level, it introduces a small increase in response size, indicating a small overhead associated with compression attempted, but not performed. When the compression level is increased to Fastest
, the effectiveness of Gzip in reducing response size becomes apparent. Moving further to the Optimal
level, Gzip demonstrates significant improvement, suggesting an ideal balance between compression speed and efficiency. At the SmallestSize
level, Gzip attempts to reduce response size as much as possible, regardless of the time required for compression.
The compression provider Brotli
shows a different behavior. At the NoCompression
level, Brotli is much more efficient than Gzip. When set at the Fastest
level, Brotli is shown to be very effective in reducing response size. Interestingly, at both the Optimal
and SmallestSize
levels, Brotli outperforms Gzip, achieving significantly better compression efficiency.
Finally, we analyze the compression provider Deflate
. Like Gzip, at the NoCompression
level it slightly increases the size of the response. Instead at the Fastest
, Optimal
, and SmallestSize
levels, Deflate achieves similar compression efficiency to Gzip.
In summary, these results underscore the effectiveness of various compression providers in optimizing ASP.NET responses. It is particularly noteworthy that Brotli emerges as the most efficient provider, especially at the Optimal
and SmallestSize
compression levels.
Conclusion
In this article, we explored optimizing ASP.NET application performance through response compression. We discussed how to configure and enable response compression, highlighting the need to balance performance with security considerations, especially in HTTPS contexts.
We also examined the use of different compression providers like Brotli and Gzip, emphasizing how the right provider choice can significantly impact performance. Additionally, we considered implementing custom compression providers for specific needs.
Concluding, through practical benchmarks, we demonstrated the effectiveness of various compression methods, underscoring the importance of choosing the most suitable solution to optimize bandwidth usage and enhance application responsiveness.
Top comments (3)
Hi @fabriziobagala !
Awesome post as usual, thanks for sharing, I wanted to ask in which scenarios would you consider positively to enable compression.
Many thanks in advance
Hi @xelit3
As always, I thank you for your support!
I would group the scenarios in which enabling compression is useful in three places:
As with anything, consider enabling compression only when the benefits outweigh the costs.
Great and crystal clear answer @fabriziobagala, thanks for it!