DEV Community

Cover image for You're importing pako to gzip data. `CompressionStream` does it natively.
Parsa Jiravand
Parsa Jiravand

Posted on • Edited on • Originally published at bestpractic.org

You're importing pako to gzip data. `CompressionStream` does it natively.

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');
Enter fullscreen mode Exit fullscreen mode

DecompressionStream is the mirror:

const stream = new DecompressionStream('gzip');
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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]);
};
Enter fullscreen mode Exit fullscreen mode

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; CompressionStream adds 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:

Top comments (2)

Collapse
 
rizzdev profile image
Andrew R

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

Collapse
 
parsajiravand profile image
Parsa Jiravand

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.