响应压缩与性能
大约 13 分钟约 3992 字
响应压缩与性能
简介
ASP.NET Core 内置响应压缩中间件(ResponseCompression),通过 Gzip 和 Brotli 算法对 HTTP 响应体进行压缩,显著减少网络传输数据量。在带宽受限或传输大体积 JSON/XML/HTML 的场景中,响应压缩是提升 API 响应速度和用户体验的有效手段。深入理解压缩算法选择、MIME 类型过滤、压缩级别权衡、最小压缩阈值以及与 HTTPS 的配合策略,有助于在生产环境中做出最优配置。
特点
压缩原理
客户端协商流程
客户端 服务器
│ │
│── GET /api/products ─────────────→│
│ Accept-Encoding: gzip, br, deflate │
│ │
│ 服务器选择 br(Brotli,客户端优先级最高)
│ 压缩响应体(JSON 100KB → 15KB)
│ │
│←── 200 OK ───────────────────────│
│ Content-Encoding: br │
│ Content-Type: application/json │
│ Vary: Accept-Encoding │
│ (响应体:15KB Brotli 压缩数据) │
│ │
│ 浏览器自动解压,JS 拿到原始 100KB │压缩算法对比
算法 压缩率 速度 CPU 消耗 浏览器支持
─────────────────────────────────────────────────
Brotli 最高 较慢 较高 现代浏览器
Gzip 中等 快 中等 所有浏览器
Deflate 较低 最快 最低 所有浏览器
典型压缩效果(JSON 响应):
原始大小: 100 KB
Gzip: 15-20 KB(压缩 80-85%)
Brotli: 10-15 KB(压缩 85-90%)
典型压缩效果(HTML):
原始大小: 200 KB
Gzip: 40-50 KB
Brotli: 30-40 KB
不适用压缩的内容:
JPEG/PNG/WebP — 已压缩的图片格式
MP4/MP3 — 已压缩的音视频格式
ZIP/GZ — 已压缩的归档格式
< 1KB 的响应 — 压缩后可能更大基础配置
快速启用
var builder = WebApplication.CreateBuilder(args);
// 最简配置
builder.Services.AddResponseCompression();
var app = builder.Build();
app.UseResponseCompression();
app.Run();
// 默认行为:
// - 支持 Gzip 压缩
// - 仅压缩 HTTPS 响应(EnableForHttps 默认 true)
// - 压缩 MIME 类型:text/plain, text/css, application/javascript,
// text/html, application/json, text/xml, application/xml
// - 最小压缩阈值:默认无限制(建议设置)推荐生产配置
builder.Services.AddResponseCompression(options =>
{
// 启用 HTTPS 压缩
// .NET 7 之前默认 false,.NET 8 默认 true
options.EnableForHttps = true;
// 添加压缩 Provider(顺序决定优先级)
// 先添加的 Provider 优先级更高
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
// MIME 类型白名单 — 只压缩这些类型
options.MimeTypes = new[]
{
// 文本类
"text/plain",
"text/css",
"text/html",
"text/csv",
"text/javascript",
"text/xml",
"text/markdown",
// 应用类
"application/json",
"application/javascript",
"application/xml",
"application/soap+xml",
"application/atom+xml",
"application/rss+xml",
"application/wasm",
"application/ld+json",
"application/manifest+json",
// 字体类
"font/woff2",
"font/woff",
"application/font-woff2",
// SVG
"image/svg+xml"
};
// 启用 HTTPS 压缩(.NET 7+)
options.EnableForHttps = true;
});
// 配置压缩级别
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
// CompressionLevel.Fastest — 最快,压缩率最低(推荐生产环境)
// CompressionLevel.Optimal — 平衡,中等压缩率和速度
// CompressionLevel.SmallestSize — 最慢,压缩率最高(不推荐,CPU 开销大)
options.Level = CompressionLevel.Fastest;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
var app = builder.Build();
app.UseResponseCompression(); // 必须在 UseRouting 之前压缩级别深入
三种级别对比
// CompressionLevel.Fastest
// - 速度最快,CPU 开销最小
// - 压缩率最低(但仍能减少 60-70% 体积)
// - 适合高并发、低延迟要求的 API
// - 推荐生产环境使用
// CompressionLevel.Optimal
// - 平衡模式,压缩率和速度折中
// - 比 Fastest 多压缩 5-10%
// - 适合中等流量的内部 API
// CompressionLevel.SmallestSize
// - 最慢,CPU 开销最大
// - 压缩率最高(比 Fastest 多压缩 10-15%)
// - 仅适合低流量、大响应、带宽极度受限的场景
// 性能测试参考(100KB JSON 响应,单次压缩):
// Fastest: ~0.3ms → 20KB (推荐)
// Optimal: ~1.5ms → 15KB
// SmallestSize: ~8.0ms → 12KB
// 选择建议:
// - 高并发 API → Fastest(CPU 是瓶颈)
// - 内部微服务 → Optimal(网络延迟更重要)
// - 静态资源(CDN)→ SmallestSize(压缩一次,多次使用)动态压缩级别选择
// 自定义 Provider:根据响应大小动态选择压缩级别
public class AdaptiveCompressionProvider : ICompressionProvider
{
public string EncodingName => "br";
public bool SupportsFlush => true;
public Stream CreateStream(Stream outputStream)
{
// 根据响应大小选择压缩级别
// 注意:CreateStream 在写入响应体之前调用
// 此时可能不知道最终大小
// 需要包装 Stream 实现动态级别切换
return new BrotliStream(outputStream, CompressionLevel.Fastest, leaveOpen: true);
}
}
// 更实用的方案:通过中间件根据响应体大小决定是否压缩
public class SizeBasedCompressionMiddleware
{
private readonly RequestDelegate _next;
private const int MinSizeToCompress = 1024; // 1KB
private const int MaxSizeToCompress = 10 * 1024 * 1024; // 10MB
public SizeBasedCompressionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var originalBodyStream = context.Response.Body;
// 使用内存缓冲区捕获响应
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await _next(context);
// 根据响应大小决定是否压缩
if (responseBody.Length >= MinSizeToCompress &&
responseBody.Length <= MaxSizeToCompress)
{
context.Response.Headers["Content-Encoding"] = "br";
context.Response.Headers["Vary"] = "Accept-Encoding";
responseBody.Position = 0;
using var compressedStream = new BrotliStream(
originalBodyStream, CompressionLevel.Fastest);
await responseBody.CopyToAsync(compressedStream);
}
else
{
responseBody.Position = 0;
await responseBody.CopyToAsync(originalBodyStream);
}
context.Response.Body = originalBodyStream;
}
}HTTPS 与压缩安全
HTTPS 压缩安全风险
// BREACH/CRIME 攻击原理:
// 攻击者通过观察压缩后响应体的大小变化,
// 逐步推断出响应中的敏感数据(如 CSRF Token)
// 攻击场景:
// 1. 响应中包含用户控制的输入(如搜索参数)
// 2. 响应中包含敏感数据(如 CSRF Token)
// 3. 攻击者通过改变输入,观察压缩后的大小变化
// 4. 大小变小 → 猜测的字符与敏感数据有重复 → 压缩率更高
// 防护策略:
// 1. 使用 Fastest 压缩级别(减少压缩率差异)
// 2. 对包含敏感数据的响应禁用压缩
// 3. 使用随机填充(Padding)消除大小特征
// 4. CSRF Token 放在响应头而非响应体中
// ASP.NET Core 默认行为:
// .NET 8: EnableForHttps = true(BREACH 攻击风险较低)
// 如果你的 API 响应不包含用户输入回显,启用 HTTPS 压缩是安全的安全的压缩配置
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
// 方案 1:排除包含敏感数据的 MIME 类型
options.MimeTypes = new[]
{
"application/json",
"text/css",
"application/javascript",
// 不压缩 text/html(HTML 通常包含 CSRF Token)
};
options.Providers.Add<BrotliCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
// 使用 Fastest 级别,减少 BREACH 攻击风险
options.Level = CompressionLevel.Fastest;
});
// 方案 2:自定义 Provider 添加随机填充
public class SafeCompressionProvider : ICompressionProvider
{
public string EncodingName => "br";
public bool SupportsFlush => true;
public Stream CreateStream(Stream outputStream)
{
return new SafeBrotliStream(outputStream);
}
}
public class SafeBrotliStream : Stream
{
private readonly BrotliStream _inner;
// 添加随机填充来消除压缩大小特征
// 实际实现较复杂,建议使用成熟的防护方案
}压缩与缓存
压缩 + 输出缓存
// 问题:ResponseCompression 和 OutputCache 一起使用时,
// 需要确保 Vary: Accept-Encoding 头正确设置,
// 否则不同压缩能力的客户端可能拿到错误的缓存
// 正确的中间件顺序
var app = builder.Build();
app.UseResponseCompression(); // 1. 压缩(先压缩)
app.UseOutputCache(); // 2. 缓存(后缓存压缩后的内容)
// OutputCache 配置 — 按 Accept-Encoding 区分缓存
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("ApiCache", policy =>
{
policy.SetVaryByHeader("Accept-Encoding"); // 关键!
policy.Expire(TimeSpan.FromMinutes(5));
});
});
// 原理:
// 1. 第一个请求(Accept-Encoding: br)→ 压缩后缓存
// 2. 第二个请求(Accept-Encoding: br)→ 直接返回缓存
// 3. 第三个请求(Accept-Encoding: gzip)→ 重新压缩并缓存压缩与响应头
// 压缩中间件会自动添加以下响应头:
// Content-Encoding: br (或 gzip)
// 指示客户端使用 Brotli(或 Gzip)解压
// Vary: Accept-Encoding
// 告知缓存代理按 Accept-Encoding 区分缓存版本
// 没有这个头,所有客户端会拿到同一个压缩版本
// Content-Length 会自动更新为压缩后的大小
// 客户端看到的 Content-Length 是压缩后的大小
// 不是原始大小
// 注意事项:
// 1. 不要手动设置 Content-Encoding,中间件会处理
// 2. 不要在压缩中间件之后修改响应体
// 3. 如果响应已经设置了 Content-Encoding(如 CDN 回源),
// 中间件不会重复压缩自定义压缩 Provider
实现自定义 Provider
// 自定义 Provider:仅在响应体超过阈值时压缩
public class ThresholdCompressionProvider : ICompressionProvider
{
private readonly int _thresholdBytes;
private readonly CompressionLevel _level;
public ThresholdCompressionProvider(int thresholdBytes, CompressionLevel level)
{
_thresholdBytes = thresholdBytes;
_level = level;
}
public string EncodingName => "br";
public bool SupportsFlush => true;
public Stream CreateStream(Stream outputStream)
{
return new ThresholdCompressionStream(outputStream, _thresholdBytes, _level);
}
}
public class ThresholdCompressionStream : Stream
{
private readonly Stream _innerStream;
private readonly MemoryStream _buffer;
private readonly int _thresholdBytes;
private readonly CompressionLevel _level;
private bool _thresholdExceeded;
private bool _disposed;
public ThresholdCompressionStream(Stream innerStream, int threshold, CompressionLevel level)
{
_innerStream = innerStream;
_thresholdBytes = threshold;
_level = level;
_buffer = new MemoryStream();
_thresholdExceeded = false;
}
public override void Write(byte[] buffer, int offset, int count)
{
if (!_thresholdExceeded)
{
_buffer.Write(buffer, offset, count);
if (_buffer.Length >= _thresholdBytes)
{
_thresholdExceeded = true;
FlushBuffer();
}
}
else
{
_innerStream.Write(buffer, offset, count);
}
}
private void FlushBuffer()
{
if (_buffer.Length == 0) return;
_buffer.Position = 0;
_buffer.CopyTo(_innerStream);
_buffer.SetLength(0);
}
public override void Flush()
{
if (_thresholdExceeded) _innerStream.Flush();
else _buffer.Flush();
}
// 省略其他 Stream 抽象方法...
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => throw new NotSupportedException();
public override long Position { get; set; }
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing && !_thresholdExceeded)
{
// 阈值内:直接输出原始内容(不压缩)
_buffer.Position = 0;
_buffer.CopyTo(_innerStream);
}
_buffer.Dispose();
_disposed = true;
}
base.Dispose(disposing);
}
}性能监控
压缩指标收集
// 压缩性能监控中间件
public class CompressionMetricsMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<CompressionMetricsMiddleware> _logger;
public CompressionMetricsMiddleware(RequestDelegate next, ILogger<CompressionMetricsMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var originalBodyStream = context.Response.Body;
using var responseBuffer = new MemoryStream();
context.Response.Body = responseBuffer;
await _next(context);
var contentLength = responseBuffer.Length;
var encoding = context.Response.Headers.ContentEncoding.ToString();
if (!string.IsNullOrEmpty(encoding))
{
_logger.LogInformation(
"压缩统计: 路径={Path}, 原始大小={OriginalBytes}, 压缩算法={Encoding}, 压缩后大小={CompressedBytes}, 压缩率={Ratio:F1}%",
context.Request.Path,
contentLength,
encoding,
responseBuffer.Length,
contentLength > 0 ? (1 - (double)responseBuffer.Length / contentLength) * 100 : 0
);
}
responseBuffer.Position = 0;
await responseBuffer.CopyToAsync(originalBodyStream);
}
}
// 推荐的压缩指标:
// 1. 压缩率 = (原始大小 - 压缩大小) / 原始大小
// 2. 压缩耗时 = 压缩完成时间 - 开始时间
// 3. 压缩命中率 = 被压缩的请求数 / 总请求数
// 4. 压缩前后的 Content-Length 对比常见问题
排错清单
// 问题 1:响应没有被压缩
// 原因 A:客户端没有发送 Accept-Encoding 头
// 检查:curl -v -H "Accept-Encoding: br" http://localhost:5000/api/data
// 原因 B:MIME 类型不在白名单中
// 检查:确认响应的 Content-Type 在 MimeTypes 配置中
// 原因 C:响应已被标记为 Content-Encoding
// 检查:上游中间件是否已设置 Content-Encoding
// 原因 D:UseResponseCompression 位置不对
// 解决:确保在 UseRouting 之前注册
// 问题 2:HTTPS 下压缩不生效
// 原因:EnableForHttps = false(.NET 7 以下默认值)
// 解决:options.EnableForHttps = true
// 问题 3:压缩后响应体损坏
// 原因:中间件在压缩后修改了响应体
// 解决:确保没有任何中间件在 UseResponseCompression 之后写入响应体
// 问题 4:CPU 使用率过高
// 原因:使用了 SmallestSize 压缩级别
// 解决:改为 Fastest 级别
// 问题 5:下载速度反而变慢
// 原因:小响应(< 1KB)被压缩后反而更大
// 解决:设置最小压缩阈值 MinBodySizeToCompress不应该压缩的内容
// 1. 已压缩的格式 — 再压缩没有意义,反而增加 CPU 开销
// JPEG, PNG, WebP, GIF(图片)
// MP4, WebM, MP3(音视频)
// ZIP, GZ, 7Z, RAR(归档文件)
// WOFF2(已压缩的字体格式)
// 2. 小响应 — 压缩后可能更大(压缩头 + 字典开销)
// < 1KB 的 JSON 响应
// 简单的 API 确认响应
// 3. 流式响应 — 无法提前知道总大小
// Server-Sent Events (SSE)
// WebSocket(有自己的压缩机制)
// 大文件下载
// 4. 加密内容 — 压缩已加密的数据效果很差
// 已加密的 JSON 响应
// 5. 二进制协议 — 压缩效果差
// Protocol Buffers
// MessagePack
// gRPC(有自己的压缩机制)优点
缺点
总结
使用 AddResponseCompression() 启用响应压缩,优先 Brotli(压缩率比 Gzip 高 15-25%)。只压缩文本类 MIME 类型(JSON/HTML/CSS/JS/XML/SVG/WASM),不压缩已压缩的图片和二进制格式。压缩级别选择 Fastest 减少 CPU 开销(推荐生产环境),SmallestSize 仅适合低流量场景。小响应(< 1KB)不压缩。HTTPS 压缩在 .NET 8 默认启用,但需注意 BREACH 攻击风险。中间件顺序:UseResponseCompression 必须在 UseRouting 之前。与 OutputCache 配合时需设置 Vary: Accept-Encoding。建议在高并发 API 场景中在网关层统一启用压缩,减轻应用服务器负担。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 框架能力的真正重点是它在请求链路中的位置和对上下游的影响。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 画清执行顺序、入参来源、失败返回和日志记录点。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 知道 API 名称,却不知道它应该放在请求链路的哪个位置。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐协议选型、网关治理、端点可观测性和契约演进策略。
适用场景
- 当你准备把《响应压缩与性能》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《响应压缩与性能》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《响应压缩与性能》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《响应压缩与性能》最大的收益和代价分别是什么?
