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 (2)
the stop rule for dropping pako is when you still need a sync call site or a pre-16.4 Safari target. past that the stream path is the default and the polyfill just adds weight
Exactly. That's a good practical rule for deciding when the native API is worth the switch.
If you don't need synchronous compression and your browser support includes
CompressionStream, the native path is usually the better default—no extra dependency, less JavaScript to ship, and the browser can handle the compression work natively.The Safari compatibility point is important too. For projects supporting older Safari versions, keeping pako (or a fallback) can still be justified. But once that compatibility constraint is gone, carrying the polyfill mostly becomes unnecessary bundle weight.
I think that's a better way to frame the migration: don't drop pako just because the native API exists; drop it when your actual runtime requirements no longer need what pako provides.