Compressing data before writing it to IndexedDB or sending it over a slow connection is a real optimization. When a cached API response is too large for reliable storage, or when you need to shrink a JSON payload before POSTing it, pako.gzip() is the standard reach. But pako is a pure-JavaScript port of zlib — written in user space to fill a gap the browser had at the time. The CompressionStream API is that gap closing.
The API
CompressionStream is a transform stream — data goes in one end, compressed data comes out the other. The constructor takes a format string: 'gzip', 'deflate', or 'deflate-raw'.
const stream = new CompressionStream('gzip');
DecompressionStream is the mirror:
const stream = new DecompressionStream('gzip');
The Streams plumbing to feed data in and collect it out is the verbose part. Wrap it once:
async function compress(input, format = 'gzip') {
const stream = new Blob([input]).stream().pipeThrough(new CompressionStream(format));
return new Uint8Array(await new Response(stream).arrayBuffer());
}
async function decompress(input, format = 'gzip') {
const stream = new Blob([input]).stream().pipeThrough(new DecompressionStream(format));
return new Response(stream).text();
}
Blob.stream().pipeThrough() feeds the data into the transform stream; new Response(stream) collects the result. The idiom is three lines of plumbing written once, then call sites that read plainly.
Replacing pako
A direct swap for the most common pako usage patterns:
// Before — pako
import pako from 'pako';
const compressed = pako.gzip(jsonString); // Uint8Array
const restored = pako.ungzip(compressed, { to: 'string' });
// After — native
const compressed = await compress(jsonString); // Uint8Array
const restored = await decompress(compressed); // string
The output is the same gzip-format Uint8Array. Any system that accepts pako's output accepts the native output — the wire format is identical.
Real-world patterns
Storing large JSON in IndexedDB:
async function putCompressed(store, key, data) {
const compressed = await compress(JSON.stringify(data));
return store.put(compressed, key);
}
async function getCompressed(store, key) {
const compressed = await store.get(key);
if (!compressed) return null;
return JSON.parse(await decompress(compressed));
}
A 200 KB JSON object typically compresses to 15–30 KB with gzip. For quota-constrained storage like IndexedDB on mobile browsers, that difference matters.
Compressing in a Web Worker:
// worker.js
self.onmessage = async ({ data }) => {
const compressed = await compress(JSON.stringify(data.payload));
self.postMessage({ compressed }, [compressed.buffer]);
};
CompressionStream works in Web Workers and Service Workers — the same API, the same two-function wrapper, no restrictions. Offloading compression to a worker keeps the main thread free.
Format reference
The three supported formats map directly to pako's methods:
CompressionStream format |
pako equivalent | When to use |
|---|---|---|
'gzip' |
pako.gzip / pako.ungzip
|
HTTP transport, file storage — the standard |
'deflate' |
pako.deflate / pako.inflate
|
zlib-wrapped deflate |
'deflate-raw' |
pako.deflateRaw / pako.inflateRaw
|
raw DEFLATE, no wrapper header |
'gzip' is the right default for most use cases. Use 'deflate-raw' only when interoperating with a system that expects unwrapped DEFLATE.
Performance
CompressionStream calls into the browser's native zlib implementation — the same C code path used when the browser decompresses HTTP responses with Content-Encoding: gzip. pako re-implements that algorithm in JavaScript. The native path is measurably faster on large inputs:
- For small payloads (< 10 KB), the difference is negligible.
- For large payloads (100 KB+), native compression is typically 3–10× faster than pako, because there's no JavaScript overhead and the engine can use SIMD instructions.
- There's also no bundle cost: pako adds ~45 KB minified to your bundle;
CompressionStreamadds zero.
Node.js
CompressionStream and DecompressionStream are available in Node.js 18.0 as part of the Web Streams implementation. The two-function wrapper above works unchanged in Node 18+ — useful for isomorphic utilities that run in both browser and server environments without branching on the environment.
Node's older zlib module (zlib.gzipSync, zlib.gunzipSync) is still more ergonomic for Node-only server code. But if you're writing a shared utility that must work in both contexts, CompressionStream is the right choice.
Browser support
CompressionStream is Baseline 2023: Chrome 80 (February 2020), Firefox 113 (May 2023), Safari 16.4 (March 2023), Node.js 18. The API has been in Chromium since early 2020; Firefox and Safari joined in 2023. It's available in all currently-supported browser and runtime versions, including Web Workers and Service Workers.
🎮 Try it yourself
▶️ Open the interactive playground →
Runs right in your browser — poke at it and watch the concept react live.
🧠 Test yourself
Think it clicked? Take the 9-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
The takeaway
Search your package.json for pako. If it's there and the use case is "compress a string or buffer before storing or sending it," the native API covers you. Replace pako.gzip and pako.ungzip with the two-function wrapper above, remove the dependency, and get native compression speed and zero bundle cost. The Streams API is verbose on its own — the wrapper absorbs that cost once so every call site stays clean.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Top comments (0)