defuss-brotli
v0.1.2
Published
A pure-Rust Brotli WebAssembly package with split compressor/decompressor exports for small build sizes.
Maintainers
Readme
defuss-brotli
A split pure-Rust Brotli package for Node.js + browsers.
- Small browser decoder bundle - import only the decompressor, skip the encoder WASM entirely
- Separate heavier encoder bundle - used server-side or at build time
- Single NPM package with split exports
- Pure-Rust WebAssembly - no native addons
- Typed TypeScript API with full JSDoc
- Vitest coverage in Node and browser
Install
bun add defuss-brotliQuick Start
Compress
import { init, compressText } from "defuss-brotli/compressor";
await init();
const compressed = compressText("Hello, Brotli!", { quality: 6 });Decompress
import { init, decompressText } from "defuss-brotli/decompressor";
await init();
const text = decompressText(compressed);
// → "Hello, Brotli!"API
compress vs compressText
| Function | Input | Encoding | When to use |
|---|---|---|---|
| compress(bytes, opts) | Uint8Array | None - bytes pass through as-is | Binary data, files, pre-encoded buffers |
| compressText(text, opts) | string | TextEncoder (UTF-8) applied internally | JSON, HTML, Markdown, any JS string |
Why TextEncoder? JS strings are UTF-16 internally, but Brotli operates on
raw bytes. TextEncoder converts to canonical UTF-8 - the standard encoding for
web text. compressText handles this so you don't have to.
decompress vs decompressText
| Function | Output | Decoding | When to use |
|---|---|---|---|
| decompress(bytes, opts) | Uint8Array | None - raw bytes returned | Binary data, custom decoding |
| decompressText(bytes, opts) | string | TextDecoder (UTF-8) applied internally | Text that was compressed with compressText |
Why TextDecoder? Reverses the TextEncoder step. If the bytes are not
valid UTF-8, replacement characters (U+FFFD) are inserted rather than throwing.
Compression Options
interface BrotliCompressOptions {
quality?: number; // 0-11, default 6
lgwin?: number; // 10-24, default 22
}quality- 0 is fastest/largest, 11 is slowest/smallest. 5-7 is the sweet spot for on-the-fly web serving.lgwin- Sliding window size exponent. Larger windows find more matches at the cost of memory. 22 covers most web-text repetition patterns.
Decompression Options
interface BrotliDecompressOptions {
maxOutputSize?: number; // default 64 MiB (67_108_864)
}Hard cap on decompressed output to prevent decompression bombs. Raise explicitly if your payloads legitimately exceed 64 MiB.
Assumptions, Guarantees, Limits
Assumptions
- Compressed input was produced by a valid Brotli encoder (this library, Node.js
zlib, nginx, etc.). - Text payloads compressed with
compressTextare valid UTF-8 when decompressed.
Guarantees
compressText(s)→decompressText(...)is a perfect round-trip for any JS string.compress(b)→decompress(...)is a perfect round-trip for anyUint8Array.init()is idempotent - calling it multiple times returns the same Promise and does not reload WASM.- Importing only
defuss-brotli/decompressornever loads the encoder WASM binary.
Limits
qualitymust be an integer in 0-11 (Brotli spec hard limit).lgwinmust be an integer in 10-24 (Brotli spec hard limit).maxOutputSizedefaults to 64 MiB - the WASM module rejects output exceeding this.- Browser WASM memory is capped at ~2 GB on 64-bit platforms (practical upper bound for very large payloads).
Why the Split Exports?
The encoder and decoder are built from two separate Rust Wasm crates:
| Export | Rust crate | WASM size | Bundle (JS + WASM) | Typical use |
|---|---|---|---|---|
| defuss-brotli/compressor | brotli | 984 KB | 1.3 MB | Server-side, build pipelines |
| defuss-brotli/decompressor | brotli-decompressor | 208 KB | 276 KB | Browser clients |
Browser apps that only decompress pre-compressed data import just the decompressor, keeping the bundle small.
Benchmarks
Measured with vitest bench on the payloads shown. Numbers are ops/sec (higher is better).
Run benchmarks yourself:
bun run bench # Node.js
bun run bench:browser # Chromium via PlaywrightNode.js (Bun)
Text Compression - compressText (ops/sec, higher is better):
| Payload | Quality 1 | Quality 6 | Quality 11 | |---|---|---|---| | Short JSON (83 B) | - | 37,239 | - | | Markdown (2 KB) | 19,819 | 10,141 | 404 | | HTML (8 KB) | 5,191 | 2,704 | 18 |
Binary Compression - compress (ops/sec):
| Payload | Quality 1 | Quality 6 | Quality 11 | |---|---|---|---| | Pseudorandom (512 B) | - | 29,430 | - | | SVG (4.9 MB) | 8.0 | 4.0 | 0.15 | | PNG (2.1 MB) | 39.6 | 82.8 | 0.21 |
PNG compresses faster than SVG at q6 because already-compressed data triggers Brotli's fast-path fallback earlier, producing less work per byte.
Text Decompression - decompressText (ops/sec):
| Payload | ops/sec | |---|---| | Short JSON (83 B) | 141,241 | | Markdown (2 KB) | 47,757 | | HTML (8 KB) | 37,288 |
Binary Decompression - decompress (ops/sec):
| Payload | ops/sec | |---|---| | Pseudorandom (512 B) | 115,493 | | SVG (4.9 MB) | 30.7 | | PNG (2.1 MB) | 699 |
Browser (Chromium)
Text Compression - compressText (ops/sec, higher is better):
| Payload | Quality 1 | Quality 6 | Quality 11 | |---|---|---|---| | Short JSON (83 B) | - | 36,264 | - | | Markdown (2 KB) | 36,002 | 12,932 | 408 | | HTML (8 KB) | 8,222 | 2,535 | 19 |
Binary Compression - compress (ops/sec):
| Payload | Quality 1 | Quality 6 | Quality 11 | |---|---|---|---| | Pseudorandom (512 B) | - | 29,712 | - | | SVG (4.9 MB) | 15.9 | 4.8 | 0.15 |
Text Decompression - decompressText (ops/sec):
| Payload | ops/sec | |---|---| | Short JSON (83 B) | 157,906 | | Markdown (2 KB) | 51,946 | | HTML (8 KB) | 35,990 |
Binary Decompression - decompress (ops/sec):
| Payload | ops/sec | |---|---| | Pseudorandom (512 B) | 122,072 | | SVG (4.9 MB) | 39.7 | | PNG (2.1 MB) | 841 |
Examples
Run the quality comparison example to see compression ratios at different quality levels:
bun run exampleSample output:
════════════════════════════════════════════════════════════════════════
Short JSON (~83 B) (83 B input)
════════════════════════════════════════════════════════════════════════
Quality │ Output │ Ratio │ Compress ms │ Decompress ms
─────── │ ────────── │ ─────── │ ─────────── │ ─────────────
1 │ 87 B │ 104.8% │ 1.02 │ 0.32
4 │ 83 B │ 100.0% │ 0.55 │ 0.74
6 │ 80 B │ 96.4% │ 0.67 │ 0.16
9 │ 80 B │ 96.4% │ 2.13 │ 0.12
11 │ 85 B │ 102.4% │ 3.54 │ 0.28
════════════════════════════════════════════════════════════════════════
Pseudorandom binary (512 B) (512 B input)
════════════════════════════════════════════════════════════════════════
Quality │ Output │ Ratio │ Compress ms │ Decompress ms
─────── │ ────────── │ ─────── │ ─────────── │ ─────────────
1 │ 286 B │ 55.9% │ 0.08 │ 0.91
4 │ 278 B │ 54.3% │ 0.28 │ 0.06
6 │ 270 B │ 52.7% │ 0.27 │ 0.09
9 │ 259 B │ 50.6% │ 0.59 │ 0.05
11 │ 239 B │ 46.7% │ 2.60 │ 0.28
════════════════════════════════════════════════════════════════════════
SVG (4.9 MB) (4.9 MB input)
════════════════════════════════════════════════════════════════════════
Quality │ Output │ Ratio │ Compress ms │ Decompress ms
─────── │ ────────── │ ─────── │ ─────────── │ ─────────────
1 │ 2.8 MB │ 57.0% │ 68.27 │ 74.07
4 │ 1.9 MB │ 38.0% │ 85.92 │ 43.93
6 │ 1.8 MB │ 37.4% │ 208.86 │ 43.04
9 │ 1.8 MB │ 36.8% │ 504.55 │ 42.33
11 │ 1.7 MB │ 34.5% │ 5093.09 │ 49.22
════════════════════════════════════════════════════════════════════════
PNG (2.1 MB) (2.1 MB input)
════════════════════════════════════════════════════════════════════════
Quality │ Output │ Ratio │ Compress ms │ Decompress ms
─────── │ ────────── │ ─────── │ ─────────── │ ─────────────
1 │ 2.1 MB │ 100.1% │ 17.39 │ 11.60
4 │ 2.1 MB │ 100.0% │ 5.75 │ 11.06
6 │ 2.1 MB │ 100.0% │ 13.64 │ 10.74
9 │ 2.0 MB │ 99.0% │ 206.06 │ 23.39
11 │ 2.0 MB │ 96.2% │ 3162.39 │ 30.90
════════════════════════════════════════════════════════════════════════
All round-trips passed
════════════════════════════════════════════════════════════════════════Key takeaways:
- SVG (text-based) compresses well: 57% → 34.5% ratio as quality increases, at the cost of much longer compression times at q11.
- PNG (already compressed) barely shrinks: ~100% ratio at q1-q6, only 96% at q11 — Brotli can't improve on an already-compressed binary format.
- Short text can actually grow at low quality levels (104.8% at q1) — the Brotli header overhead exceeds the savings on tiny payloads.
Defaults
For typical web text (JSON, Markdown, HTML, CSS, JS):
quality = 6lgwin = 22
That is a sane point on the speed/ratio curve. Crank quality higher if you
need smaller output and can afford more CPU time.
Build
Prerequisites
- Bun >= 1.0
- Rust toolchain +
wasm32-unknown-unknowntarget wasm-packwasm-opt(optional, from binaryen — produces smaller WASM)
Install everything automatically (cross-platform — installs Rust/wasm-pack if missing):
make setup
# or
bun run setupBuild everything:
bun install
bun run buildWhat happens during build:
wasm-packbuilds the compressor cratewasm-packbuilds the decompressor cratewasm-opt -Ozruns if availablebun buildbundlessrc/compressor.tsandsrc/decompressor.tsintodist/tscgenerates declaration files
Test and Benchmark
Node:
bun test
bun example
bun test:e2e
bun bench
bun bench:browserBrowser:
bun run test:browserLicense
MIT
