10LOC
Web Platformintermediate

CompressionStream for browser-side gzip

Published August 10, 2027

export const gzipEncode = async (text: string): Promise<Uint8Array> => {
  const stream = new Blob([text]).stream().pipeThrough(new CompressionStream("gzip"));
  const buffer = await new Response(stream).arrayBuffer();
  return new Uint8Array(buffer);
};

export const gzipDecode = async (bytes: Uint8Array): Promise<string> => {
  // Uint8Array's .buffer is typed ArrayBufferLike (it could be a SharedArrayBuffer),
  // which BlobPart doesn't accept -- a real runtime non-issue, just a strict-mode gap.
  const stream = new Blob([bytes as unknown as BlobPart]).stream().pipeThrough(new DecompressionStream("gzip"));
  const buffer = await new Response(stream).arrayBuffer();
  return new TextDecoder().decode(buffer);
};

What

gzipEncode/gzipDecode compress and decompress bytes using CompressionStream/DecompressionStream — the browser's built-in gzip implementation, exposed as a pair of TransformStream-shaped objects.

Why it matters

Compressing data client-side used to mean shipping a library — pako or fflate compiled from zlib — just to shrink a payload before a fetch, or to gzip a blob before writing it to IndexedDB or OPFS. CompressionStream gets you the same DEFLATE/gzip codec the browser already ships for decoding Content-Encoding: gzip responses, with zero bytes of JavaScript downloaded and no WASM to instantiate. It's a stream, so it composes with everything else in the Streams ecosystem via pipeThrough — chain it after a ReadableStream of file chunks, or in front of a fetch request body, without buffering the whole payload in memory first.

How it works

  • gzipEncode wraps the input string in a Blob to get a ReadableStream cheaply (Blob.stream()), then pipes it through new CompressionStream("gzip") — the transform stream reads uncompressed chunks in and emits gzip-compressed chunks out.
  • new Response(stream).arrayBuffer() is the drain: Response accepts any ReadableStream as a body and its arrayBuffer() method collects every chunk into one contiguous buffer, which sidesteps writing a manual chunk-collection loop.
  • gzipDecode is the mirror image with DecompressionStream("gzip"), ending in TextDecoder().decode() to turn the recovered bytes back into a string.
  • Both directions go through the same Blob → stream → Response → buffer path, just with the transform swapped — that symmetry is the whole API surface.

Gotchas

  • The format string accepts "gzip", "deflate" (zlib-wrapped DEFLATE), or "deflate-raw" (headerless DEFLATE) — picking the wrong one to match data compressed elsewhere (e.g. a server using raw DEFLATE) produces a decode error, not a silent mismatch.
  • This is a general-purpose codec, not a file format library — there's no gzip header manipulation, no multi-member stream handling, and no equivalent to zlib's dictionary support. For anything beyond "compress these bytes, decompress these bytes," you still need a real library.
  • Full cross-browser support landed later than the Chrome-only headline date suggests: Chrome/Edge 80, Safari 16.4, and Firefox only as of version 113 — check your minimum supported Firefox before relying on this without a fallback.
  • Compressing something already compressed (a JPEG, a zip) typically makes it larger, not smaller — gzip works well on text (JSON, HTML) and poorly on already-entropic binary data.

Related

  • TransformStream — the general building block CompressionStream is shaped like; you can write your own for other codecs the platform doesn't ship.
  • fetch request/response streaming — pairs naturally with this: compress a request body stream before sending, or decompress a response stream as it arrives, without buffering either end.