WSS
Web Specification Studio Home
On this page
BlogPerformanceAPIsWeb StreamsPublished

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 CompressionStream and DecompressionStream APIs 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 CompressionStream instead.

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

BrowserSupport
Chrome80+
Edge80+
Safari16.4+
Firefox113+

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 Uint8Array chunks. 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() (using duplex: '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: gzip on a request does not guarantee the server will automatically decompress it. While browsers automatically decompress HTTP responses, web servers (like Nginx, Express, or Go’s net/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.

Related posts