On this page
CompressionStream API: Replace pako and zlib.js with Native GZIP
Replace heavy JavaScript compression libraries with the browser's native CompressionStream API to compress data payloads directly on the client.
Key takeaways
- The
CompressionStreamandDecompressionStreamAPIs provide native GZIP and Deflate compression directly in the browser. - Native streams replace the need for third-party dependencies like
pako, reducing JavaScript bundle size by tens of kilobytes. - Because it leverages the Web Streams API, backpressure is handled automatically, preventing memory exhaustion when processing large files.
- Supported in all modern browsers (Chrome 80+, Safari 16.4+, Firefox 113+).
When to use it
Use CompressionStream when:
- You are uploading large JSON payloads, logs, or telemetry data to a server.
- You are writing large amounts of text or binary data to local storage (like the Origin Private File System or IndexedDB).
- You are processing large files entirely on the client-side.
Don’t use it when:
- You are compressing payloads under 1KB. The compression dictionary overhead will often make the output larger than the input.
- You are trying to compress formats that are already compressed (e.g., JPEG, WebP, MP4, ZIP).
In short: If you currently import a library to compress or decompress data on the client, you can likely remove it and use the platform’s native
CompressionStreaminstead.
Audience: Developers optimizing client-side network payloads, managing large local storage architectures, or trimming JavaScript bundle sizes.
Why this API exists
Historically, compressing data before sending it to a server—or decompressing a custom binary format—required shipping a WebAssembly port or a JavaScript implementation of zlib (e.g., pako).
Shipping a compression library has drawbacks:
- Bundle bloat: Libraries add tens of kilobytes of JavaScript to your application.
- Parse/compile overhead: The browser must download, parse, and compile the JS before it can compress anything.
- Main-thread contention: Synchronous JS-based compression blocks the main thread, resulting in poor Interaction to Next Paint (INP).
The browser already contains highly optimized native implementations of gzip and deflate to handle standard HTTP compression. The CompressionStream API exposes this existing native engine to JavaScript via the Streams API. This significantly reduces main-thread blocking, as browsers typically offload stream processing to background threads.
Browser support and constraints
| Browser | Support |
|---|---|
| Chrome | 80+ |
| Edge | 80+ |
| Safari | 16.4+ |
| Firefox | 113+ |
With Safari and Firefox adopting the specification in 2023, baseline support is now sufficient for widespread production use.
Technical Caveats
- Byte Streams Only: The API only accepts and emits
Uint8Arraychunks. You cannot pipe a standard string stream directly into it; you must encode strings to bytes first. - Supported Formats: The API strictly supports
"gzip","deflate", and"deflate-raw". It does not natively support Brotli ("br") or Zstandard ("zstd"). - Streaming Upload Support: Streaming request bodies in
fetch()(usingduplex: 'half') requires HTTP/2 or HTTP/3. While Chromium has supported this since 105, Safari (18+) and Firefox (131+) support is more recent. Always feature-detect or provide a fallback. - Server Decompression is Not Automatic: Setting
Content-Encoding: gzipon a request does not guarantee the server will automatically decompress it. While browsers automatically decompress HTTP responses, web servers (like Nginx, Express, or Go’snet/http) usually require explicit middleware to decompress incoming request bodies.
Using CompressionStream
The API operates as a TransformStream. It takes a ReadableStream of uncompressed bytes, compresses it, and outputs a ReadableStream of compressed bytes. Because it is a native stream, backpressure is handled automatically—meaning you can compress a 5GB file without running out of memory.
Basic example: Compressing a string
To compress a JavaScript string, you must first encode it into a byte stream using TextEncoderStream. The resulting byte stream is then passed to the CompressionStream. Since the output is also a byte stream, Response is a convenient utility to consume it into an ArrayBuffer.
async function compressString(inputString) {
// 1. Create a readable stream from the string
const readableStream = new ReadableStream({
start(controller) {
controller.enqueue(inputString);
controller.close();
}
});
// 2. Encode to bytes, then compress
const compressedStream = readableStream
.pipeThrough(new TextEncoderStream())
.pipeThrough(new CompressionStream('gzip'));
// 3. Consume the stream into an ArrayBuffer using Response
const compressedBuffer = await new Response(compressedStream).arrayBuffer();
return new Uint8Array(compressedBuffer);
}
Uploading large JSON payloads
Sending uncompressed JSON wastes bandwidth and slows down uploads. You can compress JSON on the client and stream it directly to the server.
async function uploadCompressedLogs(logsArray) {
const jsonString = JSON.stringify(logsArray);
// A Blob is a convenient way to get a byte stream from a string
const byteStream = new Blob([jsonString]).stream();
// Pipe through the compressor
const compressedStream = byteStream.pipeThrough(new CompressionStream('gzip'));
// Upload the compressed stream directly
await fetch('/api/logs/ingest', {
method: 'POST',
headers: {
'Content-Encoding': 'gzip',
'Content-Type': 'application/json'
},
body: compressedStream,
duplex: 'half' // Required for streaming request bodies
});
}
Note: Ensure your server endpoint is configured with middleware (e.g., body-parser in Express) to decompress gzip request bodies before parsing the JSON.
Decompressing data
The inverse operation uses the DecompressionStream interface. This is useful when fetching compressed data that the browser wouldn’t automatically decompress (such as fetching a .gz file directly from a blob store).
You can significantly simplify stream consumption by letting Response.text() handle the byte-to-string decoding:
async function fetchAndDecompress(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
// Pipe the compressed response body through the Decompressor
const decompressedStream = response.body.pipeThrough(new DecompressionStream('gzip'));
// Consume the decompressed byte stream directly into text
return await new Response(decompressedStream).text();
}
Summary
By moving compression out of userland JavaScript and into native browser code, applications become lighter and faster. Drop your heavy compression dependencies and leverage the platform to process large data structures natively.
Frequently Asked Questions
How do I configure Node.js to decompress a half-duplex fetch request?
When streaming a compressed request body from the client using duplex: 'half', Node.js servers will not automatically decompress it. If you are using Express, you must explicitly configure a body parser to inflate the request before JSON parsing: app.use(express.json({ inflate: true })). For raw Node HTTP servers, you must pipe req through zlib.createGunzip().
Can CompressionStream process 10GB+ files without crashing the browser?
Yes. Because CompressionStream implements the Web Streams API standard, it processes data in chunks and relies on internal backpressure queues. As long as you pipe the output directly to a destination (like OPFS or a streaming fetch) rather than accumulating it in an ArrayBuffer, memory usage remains flat regardless of file size.
How do I polyfill CompressionStream for older iOS Safari versions without bloat?
Safari added support in version 16.4. For older versions, dynamically import a lightweight WASM or JS polyfill (like fflate) only if the native API is missing: if (!window.CompressionStream) { const { strFromU8, zlibSync } = await import('fflate'); ... }. This ensures modern browsers get zero bundle bloat while legacy devices still function.
Does CompressionStream preserve the original filename and metadata?
No. The GZIP specification allows for an optional filename header, but the browser’s CompressionStream implementation only outputs the raw compressed payload stream. If you need to preserve directory structures or filenames, you must use a ZIP archive specification, which CompressionStream does not natively support.
How do you stream a large IndexedDB blob into a CompressionStream?
Do not read the entire Blob into memory using arrayBuffer(). Instead, retrieve the File or Blob object from IndexedDB and call .stream() on it. This yields a ReadableStream<Uint8Array> that can be piped directly: idbBlob.stream().pipeThrough(new CompressionStream('gzip')).
Why does my TextEncoderStream pipe fail with non-UTF-8 encodings?
TextEncoderStream strictly encodes to UTF-8. If your legacy backend expects Latin-1 or Shift-JIS, you cannot use the native stream encoder. You must manually chunk your string, encode the chunks using a polyfilled legacy encoder, and feed the resulting Uint8Arrays into the CompressionStream.
What happens if the network drops during a half-duplex streaming upload?
If a network disconnect interrupts a fetch upload powered by a CompressionStream, the ReadableStream will automatically receive a cancellation signal. This propagates upstream, aborting the compression process and freeing the allocated memory without requiring explicit error handling on the stream itself.
From the team at
We build digital products and explore the modern web standards behind them.