# brotli-dec-wasm

[![npm](https://img.shields.io/npm/v/brotli-dec-wasm)](https://npmjs.com/package/brotli-dec-wasm)
[![npm unpacked size](https://img.shields.io/endpoint?url=https%3A%2F%2Fnpm-unpackedsize-badge.myl.workers.dev%2Fbrotli-dec-wasm&color=blue)](https://npmjs.com/package/brotli-dec-wasm)

[Brotli](https://github.com/google/brotli) decompressor for browsers and web workers with WASM, in about 200KB.

For compressors, see [Alternatives](#alternatives).

## Features

- Optimized for size, suitable for browsers and web workers.
- [Streaming](https://brotli.org/decode.html#a234) decompression.
- Drop-in replacement for [brotli-wasm](https://github.com/httptoolkit/brotli-wasm).

## Get Started

Install the package:

```sh
npm install brotli-dec-wasm
```

The default export is a promise that resolves to the WASM module:

```js
import brotliPromise from "brotli-dec-wasm";

const brotli = await brotliPromise;
const compressedData = new Uint8Array(/* ... */);
const decompressed = brotli.decompress(compressedData);
```

### Streaming decompression

Use `DecompressStream` to decompress data in chunks. Call `decompress(input, outputSize)` in a loop, handling three result codes:

- `NeedsMoreOutput`: the current input chunk produced more output than `outputSize` bytes. Call again with the remaining input (sliced by `input_offset`).
- `NeedsMoreInput`: the current input chunk is fully consumed. Feed the next chunk.
- `ResultSuccess`: decompression is complete.

```js
import brotliPromise from "brotli-dec-wasm";

const brotli = await brotliPromise;
const stream = new brotli.DecompressStream();
const chunks = [];

const compressedChunks = [new Uint8Array(/* ... */) /* ... */];
for (const chunk of compressedChunks) {
    let resultCode;
    let inputOffset = 0;
    do {
        const input = chunk.slice(inputOffset);
        const result = stream.decompress(input, 1024);
        chunks.push(result.buf);
        resultCode = result.code;
        inputOffset += result.input_offset;
    } while (resultCode === brotli.BrotliStreamResultCode.NeedsMoreOutput);
}
```

### Using with TransformStream

`DecompressStream` works with the browser `TransformStream` API for piped workflows:

```js
import brotliPromise from "brotli-dec-wasm";

const brotli = await brotliPromise;
const decompressStream = new brotli.DecompressStream();

const decompressionStream = new TransformStream({
    transform(chunk, controller) {
        let resultCode;
        let inputOffset = 0;
        do {
            const input = chunk.slice(inputOffset);
            const result = decompressStream.decompress(input, 1024);
            controller.enqueue(result.buf);
            resultCode = result.code;
            inputOffset += result.input_offset;
        } while (resultCode === brotli.BrotliStreamResultCode.NeedsMoreOutput);
        if (
            resultCode !== brotli.BrotliStreamResultCode.NeedsMoreInput &&
            resultCode !== brotli.BrotliStreamResultCode.ResultSuccess
        ) {
            controller.error(`Brotli decompression failed with code ${resultCode}`);
        }
    },
});

await compressedReadableStream.pipeThrough(decompressionStream).pipeTo(outputWritableStream);
```

### Using with a Web Worker

Decompression runs synchronously on the calling thread. For large payloads this can freeze the UI. To avoid that, run the decompression in a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API):

```js
// worker.js
import brotliPromise from "brotli-dec-wasm";

const brotli = await brotliPromise;

self.onmessage = (e) => {
    try {
        const result = brotli.decompress(new Uint8Array(e.data));
        self.postMessage(result, [result.buffer]);
    } catch (err) {
        self.postMessage({ error: err.message });
    }
};
```

```js
// main.js
const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });

function decompress(data) {
    return new Promise((resolve, reject) => {
        worker.onmessage = (e) => {
            if (e.data.error) reject(new Error(e.data.error));
            else resolve(e.data);
        };
        const buf = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
        worker.postMessage(buf, [buf]);
    });
}

const result = await decompress(compressedData);
```

### Using the WASM module directly

The JS API generated by wasm-pack and the WASM binary file are also exported for cases where you need to instantiate the WASM module with a URL:

```js
import init from "brotli-dec-wasm/web";
import wasmUrl from "brotli-dec-wasm/web/bg.wasm";
```

Check `*.asset.*` files in [`example/webpack`](./example/webpack) for examples.

### Webpack 5

Set `experiments.asyncWebAssembly: true` (or `experiments.syncWebAssembly: true` for legacy code) in your webpack config. Without this, webpack 5 will not enable WebAssembly support.

## Benchmark

Decompressing 1MB data in headless Chromium (vitest + playwright):

| Input                                                        | ops/s | mean   |
| ------------------------------------------------------------ | ----- | ------ |
| 1MB random data (incompressible, ~1MB compressed)            | ~480  | ~2.1ms |
| 1MB repeated data (highly compressible, 41 bytes compressed) | ~440  | ~2.3ms |

Environment: AMD Ryzen 7 5800H, performance governor, Linux 6.19.6.

Run `pnpm run bench` to reproduce. Fixtures are generated by `node test/gen_fixture.js`.

## Alternatives

- [brotli-wasm](https://github.com/httptoolkit/brotli-wasm): A compressor and decompressor for Brotli, supporting Node and browsers via WASM. If you need a compressor, use it.

## Security

Use version **>= 1.3.3**. Earlier versions depend on the unmaintained Rust crate `wee_alloc`, which has open serious issues.

## License

Copyright (C) 2026 Yulong Ming <i@myl7.org>.

Apache License, Version 2.0 or MIT License, at your option.
