# Compressing Data Without a Library

Source: https://www.egnworks.com/blog/compression-streams-api-explained  
Author: Jacob Val  
Published: 2026-09-24  
Updated: 2026-09-24  
Category: Frontend Architecture  
Tags: Compression Streams API, JavaScript

> A deep technical guide to the Compression Streams API, covering why compression always meant a bundled library before, CompressionStream and DecompressionStream as transform streams, the available formats, converting a compressed stream to a concrete value, why compression happens in chunks, a practical OPFS example, and browser support.

---

Compressing data before sending it to a server, or before writing it into a local database, has always meant shipping an actual compression implementation compiled to JavaScript, a real amount of bundle weight spent on something the browser itself was never able to do natively. The Compression Streams API removes that dependency entirely, adding gzip, deflate, and newer formats as a built-in, streaming primitive.

## Why Compression Always Meant a Library Before

The browser has always been able to decompress a gzip encoded HTTP response automatically, that machinery lives deep in the network stack and was never exposed to JavaScript directly. Compressing or decompressing arbitrary data from application code, a file before upload, a payload before storing it locally, had no native equivalent, so every project that needed this reached for a library like pako or fflate, pure JavaScript or WebAssembly ports of zlib, adding real weight to a bundle for functionality the platform simply did not expose.

## CompressionStream as a Transform Stream

`CompressionStream` is a `TransformStream`, which means it fits directly into the standard Streams API pipeline rather than requiring its own bespoke input and output handling.

```js
const compressedStream = sourceStream.pipeThrough(new CompressionStream("gzip"));
```

Any `ReadableStream` can be piped through it, a fetch response body, a file's stream, or a stream constructed by hand, and the result is another `ReadableStream` producing the compressed bytes as they become available.

## Piping a Stream Through It

A `Blob`, including a `File` from a file input, exposes a `.stream()` method that produces exactly the kind of `ReadableStream` `pipeThrough()` expects.

```js
async function compressFile(file) {
  const compressedStream = file.stream().pipeThrough(new CompressionStream("gzip"));
  return new Response(compressedStream).blob();
}
```

Wrapping the resulting stream in a `Response` and reading its `.blob()` is a common pattern for converting a stream back into a concrete value once compression is complete, since `Response` already knows how to consume a `ReadableStream` body and buffer it into a usable object.

## The Available Formats

`"gzip"` and `"deflate"` are the two original formats, `"deflate"` using the DEFLATE algorithm wrapped in the zlib format with a header and trailing checksum, and `"gzip"` wrapping the same underlying algorithm in its own container format instead. `"deflate-raw"` strips both the header and checksum, producing the smallest output of the three when neither is needed by whatever reads the compressed data afterward. Newer additions to the specification bring `"brotli"` and `"zstd"` as further format options, both generally producing smaller output than DEFLATE based formats at a given compression level, though support for these two specifically is still rolling out and worth checking current browser tables for before depending on them.

## Decompressing a Response

`DecompressionStream` mirrors `CompressionStream` exactly, and reversing a compression pipeline is a matter of piping through the matching format instead.

```js
async function decompressBlob(blob) {
  const decompressedStream = blob.stream().pipeThrough(new DecompressionStream("gzip"));
  return new Response(decompressedStream).text();
}
```

This is the same shape of code as compressing, only the direction of the transform and the concrete value read off the end differ, `.text()` here instead of `.blob()`, since this example expects the decompressed result to be readable text.

## Converting a Compressed Stream to a Concrete Value

Wrapping a stream in `new Response(stream)` is not compression specific, it is a general trick for turning any `ReadableStream` into something offering `.blob()`, `.text()`, `.arrayBuffer()`, or `.json()`, all of which a `Response` object already implements to consume its own body stream. This avoids manually reading chunks off a reader and concatenating them by hand for the common case of just wanting the final compressed or decompressed result as one value rather than as a stream to process incrementally.

## Why This Runs in Chunks, Not All at Once

Because both `CompressionStream` and `DecompressionStream` are proper transform streams, they process data incrementally as chunks arrive from whatever is feeding them, rather than requiring the entire input to be buffered in memory before compression can begin. Compressing a large file this way never needs to hold the full uncompressed file and the full compressed output in memory simultaneously, the pipeline naturally applies backpressure, only pulling more input once downstream consumers are ready for more output, the same flow control every other part of the Streams API already provides.

## Compressing a Request Body Before Upload

`fetch()` accepts a `ReadableStream` directly as a request body, which means a compressed payload can be sent without ever materializing the full compressed result in memory first, provided the server on the other end is told what it is receiving.

```js
async function uploadCompressed(url, data) {
  const compressedStream = new Blob([data]).stream().pipeThrough(new CompressionStream("gzip"));

  return fetch(url, {
    method: "POST",
    headers: { "Content-Encoding": "gzip" },
    body: compressedStream,
    duplex: "half",
  });
}
```

The `Content-Encoding` header tells the receiving server how to decompress the body, the same header a server already sends on a gzip compressed response, just used here in the opposite direction. `duplex: "half"` is required whenever a `fetch()` body is a stream rather than a already-buffered value, since streaming a request body is a newer capability that has to be explicitly opted into.

## A Practical Example: Compressing Before Writing to OPFS

Combining compression with the Origin Private File System covered elsewhere in this series keeps a locally cached dataset smaller on disk without needing a compression library alongside the file access code.

```js
async function writeCompressed(root, filename, data) {
  const fileHandle = await root.getFileHandle(filename, { create: true });
  const accessHandle = await fileHandle.createSyncAccessHandle();

  const compressedStream = new Blob([data]).stream().pipeThrough(new CompressionStream("gzip"));
  const compressedBytes = new Uint8Array(await new Response(compressedStream).arrayBuffer());

  accessHandle.write(compressedBytes, { at: 0 });
  accessHandle.flush();
  accessHandle.close();
}
```

Reading the file back later pipes the same bytes through `DecompressionStream` instead, giving a locally persisted cache that trades a small amount of CPU time for meaningfully less disk space, without either half of that tradeoff depending on a bundled library.

## What It Does Not Do

The Compression Streams API compresses a stream of raw bytes, it has no concept of an archive format bundling multiple files with their own directory structure the way a genuine zip library does, and it exposes no tuning knobs for compression level or dictionary settings the way some JavaScript compression libraries offer, the browser applies its own internal defaults for a given format. A project that specifically needs a multi file zip archive, or fine grained control over compression tradeoffs, still has a real reason to reach for a dedicated library on top of, or instead of, this API.

## Browser Support

`CompressionStream` and `DecompressionStream` are supported in Chrome, Edge, Firefox, and Safari for the `gzip`, `deflate`, and `deflate-raw` formats, and count as broadly available today for that core set. The newer `brotli` and `zstd` format options are still rolling out across browsers and should be feature detected rather than assumed before relying on them in production.

## Conclusion

The Compression Streams API adds the one thing a project used to need a bundled library for, actual compression and decompression of arbitrary data, directly to the platform as a pair of ordinary transform streams. Piping a file or fetch response through `CompressionStream` or `DecompressionStream` fits naturally into code that already works with the Streams API, processes data incrementally rather than all at once, and removes a dependency that existed purely to work around a gap the platform has now closed itself.

## References

[MDN: Compression Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Compression_Streams_API)

[MDN: CompressionStream](https://developer.mozilla.org/en-US/docs/Web/API/CompressionStream)

[MDN: DecompressionStream](https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream)

[Can I Use: CompressionStream](https://caniuse.com/mdn-api_compressionstream)
