Handles compression for H3
✔️ Zlib Compression: You can use zlib compression (brotli, gzip, deflate and opt-in zstd)
✔️ Stream Compression: You can use stream compressions (gzip, deflate and opt-in brotli / zstd)
✔️ Compression Detection: It uses the best compression which is accepted
✔️ h3 v1 & v2: Works with both h3 v1 and v2
✔️ Nuxt module: Drop h3-compression/nuxt into nuxt.config and configure it there
# Using npm
npm install h3-compression
# Using yarn
yarn add h3-compression
# Using pnpm
pnpm add h3-compressionimport { createServer } from 'node:http'
import { createApp, eventHandler, toNodeListener } from 'h3'
import { useCompressionStream } from 'h3-compression'
const app = createApp({ onBeforeResponse: useCompressionStream }) // or { onBeforeResponse: useCompression }
app.use(
'/',
eventHandler(() => 'Hello world!'),
)
createServer(toNodeListener(app)).listen(process.env.PORT || 3000)Example using listhen for an elegant listener:
import { createApp, eventHandler, toNodeListener } from 'h3'
import { listen } from 'listhen'
import { useCompressionStream } from 'h3-compression'
const app = createApp({ onBeforeResponse: useCompressionStream }) // or { onBeforeResponse: useCompression }
app.use(
'/',
eventHandler(() => 'Hello world!'),
)
listen(toNodeListener(app))In h3 v2 the response is an immutable web Response and the onBeforeResponse
hook was removed. Use the compression / compressionStream middleware instead — they read the
response returned by the next handler and replace it with a compressed one.
import { createServer } from 'node:http'
import { H3, toNodeHandler } from 'h3'
import { compression } from 'h3-compression'
const app = new H3()
app.use(compression()) // or app.use(compressionStream())
app.get('/', () => 'Hello world!')
createServer(toNodeHandler(app)).listen(process.env.PORT || 3000)You can also force a specific method (e.g. compression('gzip')) instead of detecting it from the
Accept-Encoding header.
The native CompressionStream
implements the WHATWG CompressionFormat enum, which only defines gzip, deflate and
deflate-raw — there is no brotli format. This package therefore streams brotli through
node:zlib instead, so it is available wherever node:zlib is (Node, and runtimes with node
compatibility), but not on pure edge runtimes.
Because brotli is noticeably more CPU-expensive per request than gzip, it is never picked
automatically. Turn it on with the brotli flag, or force it as the method:
app.use(compressionStream()) // gzip / deflate — unchanged default
app.use(compressionStream({ brotli: true })) // brotli, then gzip, then deflate
app.use(compressionStream('br')) // always brotliThe same flag works for the composable:
await useCompressionStream(event, response, { brotli: true })
// or explicitly
await useBrotliCompressionStream(event, response)Note
The brotli stream is flushed per chunk (BROTLI_OPERATION_FLUSH) so that streamed responses
stay streamed. With zlib's defaults brotli buffers the whole body until the source closes.
Zstd is supported on both paths, and is opt-in for a different reason than brotli: node:zlib
only gained zstd in Node 22.15.0 (and 23.8.0). Enabling it by default would make the
negotiated Content-Encoding depend on the Node version the app happens to run on, which is a
poor thing to discover in production. The package itself only requires Node >= 20.11.1, the same
floor as h3.
app.use(compression({ zstd: true })) // zstd, then brotli, then gzip, then deflate
app.use(compressionStream({ zstd: true, brotli: true })) // same order, streamed
await useCompression(event, response, { zstd: true })Behaviour on a runtime without zstd:
- with the
zstd: trueflag, zstd is skipped during negotiation and the next accepted encoding is used — no error, no special-casing needed in your code - when forced (
compression('zstd'),useZstdCompression), aTypeErrornaming the required Node version is thrown, because silently sending something else would be worse
Branch on it yourself with the exported predicate:
import { isZstdSupported } from 'h3-compression'
app.use(compression({ zstd: isZstdSupported() }))Add the module and you're done — it wires the right Nitro hooks, skips Nuxt's internal routes and filters by content type for you:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['h3-compression/nuxt'],
})Everything is configurable under the compression key:
export default defineNuxtConfig({
modules: ['h3-compression/nuxt'],
compression: {
enabled: true,
encoding: 'zlib', // or 'stream'
brotli: false, // stream path only — zlib always prefers brotli
zstd: false, // needs Node >= 22.15
method: undefined, // force one method instead of negotiating
contentTypes: ['text/', 'application/json', 'application/javascript', 'application/xml', 'image/svg+xml'],
exclude: ['/_nuxt', '/__nuxt'],
routeRules: true, // also compress cached (swr/isr) routes and /server/api
threshold: 0, // skip bodies smaller than this many bytes
},
})| Option | Default | What it does |
|---|---|---|
enabled |
true |
Turn compression off without removing the module |
encoding |
'zlib' |
'zlib' buffers the body; 'stream' pipes it through a compression transform |
brotli |
false |
Consider brotli when negotiating. Only meaningful for 'stream' — the zlib path already prefers brotli |
zstd |
false |
Consider zstd when negotiating. Ignored on Node < 22.15, see Zstd |
method |
– | Force one method instead of negotiating from Accept-Encoding |
contentTypes |
text, JSON, JS, XML, SVG | Prefix match against Content-Type. Set to [] to compress everything |
exclude |
['/_nuxt', '/__nuxt'] |
Path prefixes to skip. Compressing these breaks Nuxt's error page |
routeRules |
true |
Also attach to beforeResponse, which is what cached (swr/isr) routes and /server/api handlers go through |
threshold |
0 |
Skip bodies below this size — under roughly a kilobyte compression makes payloads larger. Ignored for 'stream', where the size is not known up front |
Note
contentTypes and exclude replace the defaults rather than extending them.
Spread the defaults in if you want to add to them.
The module is a convenience wrapper — the hooks are still yours to wire if you want different behaviour per route:
server/plugins/compression.ts
import { useCompression } from 'h3-compression'
export default defineNitroPlugin((nitro) => {
// Freshly rendered SSR pages.
nitro.hooks.hook('render:response', async (response, { event }) => {
// Skip internal nuxt routes (e.g. error page)
if (['/_nuxt', '/__nuxt'].some(prefix => getRequestURL(event).pathname.startsWith(prefix)))
return
if (!response.headers?.['content-type']?.startsWith('text/html'))
return
await useCompression(event, response)
})
// The `render:response` hook only runs for freshly rendered SSR pages.
// Responses served from the Nitro route cache (`routeRules` with `swr` / `isr`)
// and `/server/api` handlers go through `beforeResponse` instead.
nitro.hooks.hook('beforeResponse', async (event, response) => {
if (['/_nuxt', '/__nuxt'].some(prefix => event.path.startsWith(prefix)))
return
await useCompression(event, response)
})
})useCompression compresses string, Buffer/Uint8Array and JSON (object) bodies and
skips everything else (e.g. streams), so binary assets are left untouched. If you only
want to compress specific content types, guard on response.headers?.['content-type']
before calling it.
H3-compression has a concept of composable utilities that accept event (from eventHandler((event) => {})) as their first argument and response as their second.
useGZipCompression(event, response)useDeflateCompression(event, response)useBrotliCompression(event, response)useZstdCompression(event, response)– requires Node >= 22.15useCompression(event, response, options?)– pass{ zstd: true }to include zstd
useGZipCompressionStream(event, response)useDeflateCompressionStream(event, response)useBrotliCompressionStream(event, response)useZstdCompressionStream(event, response)– requires Node >= 22.15useCompressionStream(event, response, options?)– pass{ brotli: true }/{ zstd: true }
compression(method | options?)– middleware using zlib (brotli, gzip, deflate, opt-in zstd)compressionStream(method | options?)– stream middleware (gzip, deflate, opt-in brotli / zstd)compressResponse(event, value, method?, options?)– low-level helper returning a compressedResponsecompressResponseStream(event, value, method?, options?)– low-level stream helper returning a compressedResponseisZstdSupported()– whether the runtime can compress with zstd
h3-compression/nuxt– the Nuxt module, configured under thecompressionkey
MIT License © 2023-PRESENT Gregor Becker