@nextrush/compressionCompression
Multi-runtime response compression with Gzip, Deflate, and Brotli support.
Problem
Source & internals
Uncompressed HTTP responses waste bandwidth and slow page loads. Text-based payloads (JSON, HTML, CSS) often shrink 60–80% with compression, yet getting it right involves content negotiation, content-type filtering, threshold tuning, and runtime compatibility — details you should not have to manage per-route.
Default Behavior
With default options, the middleware:
- Enables Gzip, Deflate, and Brotli (where the runtime supports it)
- Compresses responses larger than 1024 bytes (1 KB)
- Selects the best encoding from the client's
Accept-Encodingheader - Skips already-compressed content types (images, video, archives)
- Skips
HEADrequests and204/304status codes - Adds
Vary: Accept-Encodingfor correct caching - Falls back to uncompressed responses on compression failure
Installation
$ pnpm add @nextrush/compression
Minimal Usage
import { createApp } from '@nextrush/core';
import { compression } from '@nextrush/compression';
const app = createApp();
app.use(compression());Configuration Options
Pass an options object to compression() to override defaults.
CPU Trade-off
Higher compression levels reduce payload size but increase CPU usage per response. Level 6 (default) balances size and speed for most workloads. Use level 1–3 for latency-sensitive APIs or level 9+ only for static pre-compressed assets.
app.use(
compression({
gzip: true,
deflate: true,
brotli: true,
level: 6,
threshold: 1024,
breachMitigation: false,
})
);CompressionOptions
| Property | Type | Description |
|---|---|---|
gzip? | boolean= true | Enable Gzip compression. |
deflate? | boolean= true | Enable Deflate compression. |
brotli? | boolean= true | Enable Brotli compression. Disabled automatically on runtimes with no Brotli support at all (Deno, Edge) — but NOT reliably on Bun, see the Runtime Support warning below. |
level? | number= 6 | Compression level. 0–9 for Gzip/Deflate, 0–11 for Brotli. Higher values produce smaller output but use more CPU. |
threshold? | number= 1024 | Minimum response size in bytes to compress. Responses below this size are sent uncompressed. |
contentTypes? | readonly string[]= DEFAULT_COMPRESSIBLE_TYPES | Content types to compress. Supports exact matches and wildcard patterns (e.g., text/*). |
exclude? | readonly string[]= DEFAULT_EXCLUDED_TYPES | Content types to exclude from compression. Supports exact matches and wildcard patterns. |
filter? | (ctx: Context) => boolean | Custom filter function. Return true to compress, false to skip. |
breachMitigation? | boolean= false | Add random padding to prevent BREACH compression ratio analysis attacks. |
Algorithm-Specific Middleware
Use dedicated functions when you need a single compression algorithm:
import { gzip, deflate, brotli } from '@nextrush/compression';
app.use(gzip({ level: 6 })); // Gzip only — universal support
app.use(deflate({ level: 6 })); // Deflate only — fast compression
app.use(brotli({ level: 4 })); // Brotli only — best ratio (Node.js; see Runtime Support for Bun)These accept the same options as compression(), minus the gzip, deflate, and brotli toggles.
Content Negotiation
The middleware parses the Accept-Encoding header and selects the best encoding the server supports. Priority order when quality values are equal:
- Brotli (
br) — best compression ratio - Gzip (
gzip) — widest client support - Deflate (
deflate) — fallback
// Client: Accept-Encoding: gzip, br;q=0.9
// Server responds with Gzip (higher quality value)
app.use(compression());BREACH Attack Mitigation
Responses containing secrets alongside user-controlled input are vulnerable to BREACH attacks. Enable random padding to mitigate:
import { compression, secureCompressionOptions } from '@nextrush/compression';
app.use(compression(secureCompressionOptions()));
// With additional overrides
app.use(compression(secureCompressionOptions({ threshold: 512 })));secureCompressionOptions() returns a CompressionOptions object with breachMitigation: true and a default level of 4.
Integration Example
Track compression statistics in downstream middleware:
import { compression, getCompressionInfo, wasCompressed } from '@nextrush/compression';
app.use(compression());
app.use(async (ctx) => {
await ctx.next();
if (wasCompressed(ctx)) {
const info = getCompressionInfo(ctx);
console.log(`${info?.encoding}: ${info?.originalSize} → ${info?.compressedSize} bytes`);
}
});Runtime Support
| Runtime | Gzip | Deflate | Brotli |
|---|---|---|---|
| Node.js | ✅ | ✅ | ✅ |
| Bun | ✅ | ✅ | ⚠️ Reports supported, fails closed |
| Deno | ✅ | ✅ | ❌ |
| Edge | ✅ | ✅ | ❌ |
Brotli is broken on Bun, not merely unsupported
detectCapabilities() hardcodes hasBrotli: true whenever it detects Bun, but never verifies
that Bun's CompressionStream actually implements the br format — and Bun has no node:zlib
fallback either. If a client's Accept-Encoding negotiates Brotli on Bun, compress() throws
internally; the middleware catches it, degrades to sending the response uncompressed, and
records the failure on ctx.state.compressionError. Disable Brotli explicitly on Bun
(compression({ brotli: false })) — gzip and deflate work correctly there.
Common Mistakes
Setting threshold too low. Compressing responses under ~150 bytes often produces output larger than the original. Keep the default of 1024 bytes unless you have measured a benefit.
Compressing already-compressed content. The middleware skips excluded types by default (PNG, JPEG, ZIP, etc.). If you override contentTypes, make sure you do not include formats that are already compressed.
Using high Brotli levels for dynamic content. Brotli levels 10–11 are designed for offline pre-compression. For dynamic responses, level 4–6 provides a good speed/ratio balance.
Troubleshooting
Responses are not compressed. Verify that the response body exceeds the threshold, the Content-Type matches an entry in contentTypes, and the client sends an Accept-Encoding header.
Double compression. If Content-Encoding is already set on the response, the middleware skips compression. Check that no upstream middleware is adding this header.
Brotli not activating (Node.js/other runtimes). Brotli requires node:zlib or a CompressionStream implementation that supports br. Call detectCapabilities() to inspect what the current runtime reports.
On Bun, Brotli negotiates but the response comes back uncompressed. This is a known runtime gap, not a misconfiguration — see the callout above. Set compression({ brotli: false }) on Bun, or check ctx.state.compressionError to confirm.
Related
- Middleware Overview — All middleware packages
- @nextrush/body-parser — Request body parsing
- @nextrush/helmet — Security headers