npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

defuss-brotli

v0.1.2

Published

A pure-Rust Brotli WebAssembly package with split compressor/decompressor exports for small build sizes.

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-brotli

Quick 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 compressText are 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 any Uint8Array.
  • init() is idempotent - calling it multiple times returns the same Promise and does not reload WASM.
  • Importing only defuss-brotli/decompressor never loads the encoder WASM binary.

Limits

  • quality must be an integer in 0-11 (Brotli spec hard limit).
  • lgwin must be an integer in 10-24 (Brotli spec hard limit).
  • maxOutputSize defaults 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 Playwright

Node.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 example

Sample 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 = 6
  • lgwin = 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-unknown target
  • wasm-pack
  • wasm-opt (optional, from binaryen — produces smaller WASM)

Install everything automatically (cross-platform — installs Rust/wasm-pack if missing):

make setup
# or
bun run setup

Build everything:

bun install
bun run build

What happens during build:

  1. wasm-pack builds the compressor crate
  2. wasm-pack builds the decompressor crate
  3. wasm-opt -Oz runs if available
  4. bun build bundles src/compressor.ts and src/decompressor.ts into dist/
  5. tsc generates declaration files

Test and Benchmark

Node:

bun test
bun example
bun test:e2e

bun bench
bun bench:browser

Browser:

bun run test:browser

License

MIT