DEV Community

Fabrizio Bagalà
Fabrizio Bagalà

Posted on • Updated on

Response Compression in ASP.NET

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:

  1. Use AddResponseCompression to add the response compression service to the service container.
  2. 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();
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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:

  1. BrotliCompressionProvider, using the Brotli algorithm.
  2. 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>();
});
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 the DeflateStream 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);
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 as text/*, 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();
Enter fullscreen mode Exit fullscreen mode

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)
    })));
Enter fullscreen mode Exit fullscreen mode

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.

References

Top comments (3)

Collapse
 
xaberue profile image
Xavier Abelaira Rueda

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

Collapse
 
fabriziobagala profile image
Fabrizio Bagalà • Edited

Hi @xelit3
As always, I thank you for your support!

I would group the scenarios in which enabling compression is useful in three places:

  1. If your users are in areas with slow Internet connections or if they use data-limited mobile devices, compression can help reduce loading times and save data.
  2. When a REST API or GraphQL returns a large amount of JSON or XML data.
  3. If you are providing large static content, such as CSS, JavaScript, or images. It obviously does not make sense to compress natively compressed assets, such as PNG files. When you try to further compress a natively compressed response, any small additional reduction in size and throughput time will likely be overshadowed by the time it takes to process the compression.

As with anything, consider enabling compression only when the benefits outweigh the costs.

Collapse
 
xaberue profile image
Xavier Abelaira Rueda

Great and crystal clear answer @fabriziobagala, thanks for it!