Using DecompressionStream to Unpack Data in JavaScript

The DecompressionStream interface is a built-in web API in modern JavaScript that decompresses streams of binary data natively in the browser and Node.js environments. By leveraging the Compression Streams API, developers can unpack formats like Gzip and Deflate on the fly without relying on heavy third-party libraries. This article explains the underlying mechanism of DecompressionStream, demonstrates how to pipe compressed binary data through it, and outlines best practices for handling both network responses and in-memory byte buffers.

How DecompressionStream Works

DecompressionStream implements the standard TransformStream interface. It sits between a readable source of compressed bytes and a writable destination, decoding incoming binary chunks sequentially using native, low-level browser routines (written in C++ or Rust). Because it operates as a stream, it processes data chunk-by-chunk rather than loading the entire compressed payload into memory at once, significantly reducing memory overhead and latency.

The constructor accepts a format string specifying the compression algorithm: * 'gzip': For data compressed using the Gzip format. * 'deflate': For data compressed using the ZLIB compression format with headers. * 'deflate-raw': For raw Deflate streams without ZLIB headers or checksums.

Decompressing a Network Stream (fetch)

When downloading compressed assets, you can pipe the HTTP response body directly into a DecompressionStream. This allows you to read the unpacked text or binary data as it arrives over the network.

async function fetchAndDecompress(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  // Pipe the raw response stream through the Gzip decompressor
  const decompressedStream = response.body.pipeThrough(
    new DecompressionStream('gzip')
  );

  // Consume the decompressed stream as text
  const text = await new Response(decompressedStream).text();
  return text;
}

Decompressing In-Memory Binary Buffers

If you already have compressed binary data stored in an ArrayBuffer or Uint8Array, you can wrap it in a Blob or ReadableStream to process it with DecompressionStream.

async function decompressByteArray(compressedBytes, format = 'gzip') {
  // Create a stream from the compressed byte array
  const stream = new Blob([compressedBytes]).stream();

  // Pipe through the decompression transform stream
  const decompressedStream = stream.pipeThrough(
    new DecompressionStream(format)
  );

  // Read the decompressed data into an ArrayBuffer
  const decompressedBuffer = await new Response(decompressedStream).arrayBuffer();
  return new Uint8Array(decompressedBuffer);
}

Reading Chunks Manually with a Stream Reader

For granular control over data processing, you can attach a reader to the readable side of the DecompressionStream to process decompressed chunks incrementally.

async function processStreamChunks(compressedStream) {
  const decompressionStream = new DecompressionStream('gzip');
  const readable = compressedStream.pipeThrough(decompressionStream);
  const reader = readable.getReader();

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    // 'value' is a Uint8Array containing a decompressed data chunk
    console.log(`Received decompressed chunk of size: ${value.byteLength}`);
  }
}

Key Advantages

  1. Zero External Dependencies: Eliminates the need for external libraries like pako or fflate, reducing JavaScript bundle sizes.
  2. Memory Efficiency: Avoids high memory spikes by unpacking data incrementally rather than buffering whole archives into RAM.
  3. High Performance: Native engine-level decompression executes significantly faster than equivalent WebAssembly or pure JavaScript decompression routines.