JavaScript Compression Streams Table
| Piece | What it does | Field note |
|---|---|---|
new CompressionStream('gzip') | A compressing pipe | gzip / deflate / deflate-raw - the three formats |
blob.stream().pipeThrough(cs) | Compose the flow | Blob in, compressed stream out - no manual chunk loops |
new Response(cs.readable).blob() | Collect the result | The read side wraps into a Blob for saving/sending |
DecompressionStream | The mirror | Same API, reversed - roundtrip in 4 lines each way |
Streams composition | Backpressure included | Works with fetch/file streams - pipeTo uploads compressed |
Server must know | The pairing contract | Compressing what the server already gzips DOUBLE-compresses |
Client-side win | Before upload | Big JSON payloads shrink 5-10x pre-send on slow uplinks |
NOT a zip archive | Single stream only | No multi-file containers - that is still a library |
CompressionStream brings gzip to the client as a PIPE: blob.stream().pipeThrough(new CompressionStream('gzip')) and the bytes flow through compressed - composed with fetch and file streams, with backpressure handled by the stream machinery.
Bottom line: the win is client-side pre-send compression - a big JSON payload shrinks 5-10x before a slow uplink carries it - and local compression for storage. The two contracts to respect: the server must know not to double-compress what arrives already gzipped, and this is a single stream, not a multi-file zip archive.
The honest part: the roundtrip is four lines each way because the API has no options - no compression levels, no archive structure, no per-file entries. It is a codec pipe; zip libraries still own containers, and HTTP transfer-encoding still owns transport compression.
How to use
- Compress before upload: const gz = await new Response(blob.stream().pipeThrough(new CompressionStream('gzip'))).blob(); - send gz with a Content-Encoding hint header.
- Roundtrip in four lines: compress through CompressionStream, collect via Response blob; reverse through DecompressionStream - symmetric, no options to remember.
- Compose with other stages: stream.pipeThrough(compressor).pipeThrough(encryptor).pipeTo(upload) - ordering is the design (compress then encrypt).
Frequently asked questions
Why compress on the client when servers gzip responses?
Different direction, different owner. Server gzip compresses RESPONSES going to the client - universal and free. Client-side compression compresses what the CLIENT SENDS: uploads on residential and mobile uplinks are far slower than downlinks, and a 10MB JSON payload shrinking to 1MB before the send is often the difference between a snappy save and a spinner. It also reduces storage costs for client-archived data and enables compressed data exchange between clients (through a server that just relays). The contract that comes with it: the receiving server must KNOW the payload is gzipped - decompress on receipt instead of applying its own response gzip to already-compressed bytes, which wastes CPU and can even grow data.
How does the pipe composition actually work end to end?
Every piece is a stream. The source (a Blob's .stream(), a fetch response body, a file handle stream) pipes THROUGH CompressionStream - a TransformStream that gzips chunks as they pass - and into a sink: another pipe, a Response for collection, or pipeTo a writable. Backpressure is native: a slow sink slows the compressor slows the source, so memory stays bounded on multi-gigabyte inputs. The collect idiom - new Response(compressedStream).blob() - wraps the readable in Response purely to reuse its blob() convenience. Ordering matters when composing: compress then encrypt (encryption's output is incompressible noise, so the reverse order wastes the compression).
What are the real limits - why is this not a zip library?
The API is a single-stream codec: one format (gzip, deflate, deflate-raw), no options, no container. There is no way to add multiple FILES with names and directories (zip's container format), no compression-level tuning (the implementation picks), no header/comment metadata, and no streaming random access into an archive. Those needs - bundling, partial extraction, encryption-aware containers - remain zip library territory (JSZip, fflate). The compression API's niche is exactly one continuous byte flow: a big JSON, a generated report, a serialized state snapshot. Within that niche it wins on being native, streaming and dependency-free.
When does client-side compression actually pay off?
When the bytes are large, compressible, and leave the client. The winning cases: exporting big JSON/table data (5-10x on structured data), saving application state snapshots to IndexedDB or cloud storage, and P2P or relayed data exchange. The losing cases: already-compressed data (JPEG, video, existing gzip - recompressing adds CPU and can grow size), tiny payloads (overhead dominates), and server uploads where the server re-compresses anyway. The check before building: sample the data, run it through CompressionStream, and compare sizes - the API makes that experiment four lines, which is the right way to justify or skip the feature.