npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pronghorn/compress

v0.1.0

Published

Response compression middleware for Pronghorn, built on Bun's native CompressionStream. Negotiates brotli, gzip, and deflate against Accept-Encoding with zero external dependencies.

Downloads

26

Readme

Compress 🗜️

Compress is a lightweight, TypeScript-first response compression middleware built as an external plugin for Pronghorn. It negotiates brotli, gzip, and deflate against a request's Accept-Encoding header and streams the response body through Bun's native CompressionStream, with zero external dependencies.

Built as a standalone package (@pronghorn/compress), the Pronghorn equivalent of fastify-compress, powered entirely by Web-standard streams rather than zlib bindings.

Why Compress

Pronghorn had no way to shrink response payloads before this. Bun v1.3.3 shipped native CompressionStream/DecompressionStream support for gzip, deflate, brotli, and zstd, which means compression can be implemented as pure stream-piping with no native bindings or third-party libraries at all.

  • Negotiates the best encoding the client supports via Accept-Encoding, preferring brotli, then gzip, then deflate.
  • Streams the response body through CompressionStream, no buffering the whole payload in memory first.
  • Skips payloads below a configurable threshold, compressing tiny responses wastes CPU for negligible savings.
  • Skips already-compressed or binary content types (images, video, zip) by default via a MIME allowlist.
  • Automatically sets Content-Encoding, removes the now-inaccurate Content-Length, and appends Vary: Accept-Encoding for correct caching behavior.
  • Zero runtime dependencies, pronghorn is only a peer dependency for types.

Installation

bun add @pronghorn/compress

Requires Bun >=1.3.3 (the version that introduced native CompressionStream support) and pronghorn >=0.1.2 as a peer dependency (used for typing the middleware only).

Quick Start

import { createApp } from 'pronghorn'
import { compress } from '@pronghorn/compress'

const app = createApp()

app.use(compress())

app.get('/data', context => context.json({ items: Array.from({ length: 500 }, (_, i) => i) }))

await app.listen(4000)

Register compress last (or near-last) in the global middleware chain, after shield/cors, so it compresses the fully-formed response rather than an intermediate one.

Core Concepts

Encoding negotiation

Compress reads the request's Accept-Encoding header and picks the highest-priority encoding both the client accepts and the server allows. If the client sends *, the server's top preference is used; if no supported encoding is found, the response passes through uncompressed.

app.use(compress({ encodings: ['br', 'gzip'] })) // drop deflate entirely

Size threshold

Very small responses (a short JSON error, a health check) rarely benefit from compression, the overhead can even make them larger. Compress skips any response reporting a Content-Length below the threshold.

app.use(compress({ threshold: 2048 })) // only compress responses ≥ 2KB

Responses without a known Content-Length (e.g. already streamed) are still compressed, since their size can't be checked upfront.

MIME type filtering

By default, only text-based and structured formats are compressed: text/*, application/json, application/javascript, application/xml, and image/svg+xml. Binary formats like JPEG, PNG, or ZIP are already compressed and excluded automatically.

app.use(compress({
  mimeTypes: ['text/', 'application/json', 'application/vnd.api+json']
}))

Streaming, not buffering

The response body is piped directly through CompressionStream without ever being fully loaded into memory, keeping compression overhead flat regardless of payload size.

app.get('/export.csv', context => {
  const stream = generateCsvStream() // any ReadableStream
  return new Response(stream, { headers: { 'Content-Type': 'text/csv' } })
})
// compress() will transparently compress this if the client accepts it

Middleware Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | threshold | number | 1024 | Minimum response size in bytes before compression is applied | | encodings | Encoding[] | ['br', 'gzip', 'deflate'] | Allowed encodings, in priority order | | mimeTypes | string[] | ['text/', 'application/json', 'application/javascript', 'application/xml', 'image/svg+xml'] | Content-Type substrings eligible for compression |

Encoding is 'br' | 'gzip' | 'deflate'.

API Reference

compress(options?: CompressOptions): Middleware - global middleware factory, register via app.use(compress(options)).

Lower-level negotiation helpers are also exported for advanced use outside the middleware:

import { negotiateEncoding, isCompressible } from '@pronghorn/compress'

const encoding = negotiateEncoding(request.headers.get('accept-encoding'), ['br', 'gzip'])
const shouldCompress = isCompressible(response.headers.get('content-type'), ['text/', 'application/json'])

Architecture

Compress is split into two modules, each with a single responsibility.

| Module | Responsibility | | --- | --- | | negotiate.ts | Parses Accept-Encoding, resolves the best allowed encoding, and checks content-type eligibility | | middleware.ts | Applies threshold/MIME filtering, pipes the body through CompressionStream, and rebuilds response headers |

Because a compressed stream's final byte length isn't known until the stream finishes, Content-Length is removed rather than recalculated, letting the underlying transport handle chunked delivery, the same approach fastify-compress uses for streamed responses.

Performance Notes

  • Brotli generally compresses better than gzip for text content but is slightly more CPU-intensive; both are supported natively by Bun with no bindings overhead.
  • Because compression is streamed rather than buffered, memory usage stays flat even for large responses, letting you compress exports or large JSON payloads safely.
  • Skipping already-compressed MIME types (images, video, archives) avoids burning CPU cycles for zero size benefit, re-compressing binary formats can occasionally make them larger.

License

WTFPL (Do What the Fuck You Want to Public License), see LICENSE for details.