压缩中间件
要点
compress()中间件对响应体进行 gzip 或 deflate 压缩,减少传输体积- 压缩只对有
Accept-Encoding: gzip头的请求生效 - 小于阈值的响应不压缩(压缩小文件反而更大)
- 已经压缩的内容(图片、视频)不重复压缩
- 压缩消耗 CPU,适合文本类响应(JSON、HTML、CSS)
- CDN 和反向代理(Nginx)通常已经处理压缩,应用层可以不做
1. 基本用法
compress() 中间件压缩响应体:
// src/index.ts
import { Hono } from 'hono'
import { compress } from 'hono/compress'
const app = new Hono()
app.use('*', compress())
app.get('/api/large-data', (c) => {
// 返回大 JSON 响应
const data = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: 'This is a long description that benefits from compression',
}))
return c.json(data)
})
export default app客户端请求带 Accept-Encoding: gzip 时,响应自动压缩:
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: application/json
Vary: Accept-Encoding
[gzip compressed data]压缩后传输体积可能减少 60-80%,显著降低带宽消耗和传输延迟。
2. 压缩算法
compress() 支持两种算法:
2.1 gzip
最常用的压缩算法,兼容性好:
app.use('*', compress({
encoding: 'gzip',
}))2.2 deflate
压缩率稍高,但兼容性不如 gzip:
app.use('*', compress({
encoding: 'deflate',
}))默认 compress() 会根据客户端 Accept-Encoding 头选择最优算法(优先 gzip)。
现代浏览器还支持 Brotli(br),但 Hono 内置的 compress() 不支持 Brotli。如果需要 Brotli,可以使用第三方库或在 CDN/Nginx 层面处理。
3. 压缩阈值
默认 compress() 对大于 1KB 的响应压缩。小于阈值的响应不压缩(压缩后可能更大):
app.use('*', compress({
threshold: 1024, // 1KB 以下不压缩
}))调整阈值需要根据实际响应大小权衡:
- 阈值太低:小文件压缩后可能更大,浪费 CPU
- 阈值太高:中等大小的文件错过压缩机会
常见值是 1KB-10KB。
4. 不压缩的内容类型
某些内容类型不适合压缩:
app.use('*', compress({
// 不压缩已经压缩过的内容
skip: (c) => {
const contentType = c.res.headers.get('Content-Type') ?? ''
// 图片、视频、字体等已经压缩过
return contentType.startsWith('image/')
|| contentType.startsWith('video/')
|| contentType.startsWith('font/')
},
}))适合压缩的内容类型:
application/jsontext/htmltext/csstext/plainapplication/javascript
不适合压缩的内容类型:
image/jpeg、image/png、image/webp(已经压缩)video/mp4、audio/mpeg(已经压缩)font/woff2(已经压缩)application/pdf(部分压缩)application/zip(已经压缩)
5. 压缩与缓存
压缩后的响应可以被缓存,但需要注意 Vary 头:
Vary: Accept-EncodingVary 告诉 CDN 和浏览器,响应内容取决于 Accept-Encoding 头。同一个 URL,带 gzip 和不带 gzip 的请求,返回不同的缓存版本。
如果 CDN 忽略了 Vary 头,可能把压缩版本返回给不支持 gzip 的客户端,导致乱码。
5.1 压缩在缓存之前
如果先缓存后压缩,同一个响应只需要压缩一次:
// 先 cache,后 compress
app.use('/api/*', cache({ cacheControl: 'max-age=3600' }))
app.use('/api/*', compress())但这样缓存的是未压缩版本,每个客户端请求都要重新压缩。
5.2 压缩在缓存之前(应用层)
如果先压缩后缓存,缓存的是压缩版本,不同 Accept-Encoding 的客户端需要不同的缓存:
// 先 compress,后 cache
app.use('/api/*', compress())
app.use('/api/*', cache({ cacheControl: 'max-age=3600' }))需要 Vary: Accept-Encoding 头让缓存正确区分。
实际选择取决于部署架构。如果 CDN 已经处理了压缩,应用层可以只做缓存不做压缩。
6. 压缩性能
压缩消耗 CPU。高并发场景下,压缩可能成为瓶颈:
// 监控压缩耗时
app.use('*', async (c, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
if (c.res.headers.get('Content-Encoding') === 'gzip') {
console.log(`Compression took ${duration}ms`)
}
})优化方向:
- 提高阈值:小文件不压缩,减少 CPU 消耗
- 使用更快的算法:gzip level 1(最快)vs level 9(最高压缩率)
- 在 CDN 层压缩:应用层不压缩,交给 CDN 处理
- 预压缩:构建时压缩静态文件,运行时直接返回
7. 与 CDN 和反向代理的配合
大部分 CDN(Cloudflare、CloudFront)和反向代理(Nginx、Caddy)默认开启压缩。如果上游已经压缩,应用层再压缩是浪费。
判断是否需要应用层压缩:
- CDN 开启压缩:应用层不需要压缩
- Nginx 开启 gzip:应用层不需要压缩
- 直接暴露应用:应用层需要压缩
// 只在直接暴露应用时开启压缩
if (process.env.DEPLOY_MODE === 'direct') {
app.use('*', compress())
}8. 压缩与流式响应
流式响应(c.stream())也可以压缩,但需要注意缓冲区:
app.get('/api/stream', compress(), (c) => {
return c.stream(async (stream) => {
for (let i = 0; i < 100; i++) {
await stream.write(`data: chunk ${i}\n\n`)
await new Promise((resolve) => setTimeout(resolve, 100))
}
})
})压缩中间件会缓冲数据,直到达到一定大小或流结束才发送。对于实时性要求高的流式响应,可能需要调整缓冲区大小或禁用压缩。
9. 测试压缩
// tests/compress.test.ts
import { describe, it, expect } from 'vitest'
import { Hono } from 'hono'
import { compress } from 'hono/compress'
describe('compress middleware', () => {
it('压缩响应体', async () => {
const app = new Hono()
app.use('*', compress())
app.get('/', (c) => c.text('a'.repeat(10000)))
const res = await app.request('/', {
headers: { 'Accept-Encoding': 'gzip' },
})
expect(res.headers.get('Content-Encoding')).toBe('gzip')
expect(res.headers.get('Vary')).toBe('Accept-Encoding')
})
it('小响应不压缩', async () => {
const app = new Hono()
app.use('*', compress({ threshold: 1024 }))
app.get('/', (c) => c.text('small'))
const res = await app.request('/', {
headers: { 'Accept-Encoding': 'gzip' },
})
expect(res.headers.get('Content-Encoding')).toBeNull()
})
it('不支持 gzip 的客户端不压缩', async () => {
const app = new Hono()
app.use('*', compress())
app.get('/', (c) => c.text('a'.repeat(10000)))
const res = await app.request('/') // 没有 Accept-Encoding 头
expect(res.headers.get('Content-Encoding')).toBeNull()
})
})总结
压缩中间件减少响应体的传输体积,降低带宽消耗和传输延迟。Hono 内置的 compress() 中间件支持 gzip 和 deflate 算法。
这一节涉及到的几个层次:
- 基本用法:
compress()自动压缩响应体 - 压缩算法:gzip(默认)vs deflate,Brotli 需要第三方支持
- 压缩阈值:小文件不压缩,避免压缩后更大
- 内容类型:文本类适合压缩,图片/视频/字体不适合
- 压缩与缓存:
Vary: Accept-Encoding确保缓存正确区分 - 性能考量:压缩消耗 CPU,高并发场景需要权衡
- 与 CDN 配合:CDN 已开启压缩时,应用层可以不做
压缩是一个低成本的优化。如果部署架构里没有 CDN 或反向代理处理压缩,应用层开启 compress() 可以显著减少带宽消耗。
下一篇看中间件组合模式——工厂模式、compose、依赖注入、作用域隔离。