Response Compression in ASP.NET

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://dev.to/fabriziobagala/response-compression-in-aspnet-8ba

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.

⚠️ Warning
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:

  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>();
});

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 LevelValueDescription
Optimal0The compression operation should optimally balance compression speed and output size.
Fastest1The compression operation should complete as quickly as possible, even if the resulting file is not optimally compressed.
NoCompression2No compression should be performed on the file.
SmallestSize3The 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 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);
}

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 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();

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:

ProviderCompression levelResponse sizePercentage decrease
NoneNone28,93 KBbaseline
GzipNoCompression29,00 KB-0.2% (increase)
GzipFastest3,60 KB87,6%
GzipOptimal3,32 KB88,5%
GzipSmallestSize3,06 KB89,4%
BrotliNoCompression4,14 KB85,7%
BrotliFastest3,29 KB88,6%
BrotliOptimal1,74 KB94,0%
BrotliSmallestSize1,74 KB94,0%
DeflateNoCompression28,99 KB-0.2% (increase)
DeflateFastest3,57 KB87,7%
DeflateOptimal3,29 KB88,6%
DeflateSmallestSize3,04 KB89,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 FastestOptimal, 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 conclusion, while response compression in ASP.NET is a potent tool for enhancing web application performance, it comes with its considerations. Although it can significantly reduce the size of transferred data, it also incurs a CPU cost as data must be compressed before being sent to the client. As such, response compression might not always be the optimal choice, especially for applications with high CPU loads or small-sized responses where the benefits of compression could be minimal.

Therefore, as with any tool, response compression needs to be used judiciously, and its configuration needs to be carefully optimized to suit the specific needs of your application. Understanding the trade-offs between CPU usage and bandwidth savings is key to deploying effective response compression in your ASP.NET applications.

References