Library / SDK
foliojs/brotli.js avatar
foliojs/brotli.js

brotli.js: Brotli compression in pure JavaScript

A JavaScript port of the Brotli compression algorithm, as used in WOFF2

538 stars55 forksJavaScriptLicense varies

At a glance

What is it?
brotli.js is a JavaScript port of the Brotli algorithm, with a hand ported decompressor and an Emscripten compiled compressor, published as an ESM-only package with TypeScript declarations and separate compress and decompress entry points. It suits a browser or Node project that needs Brotli without a native binary, and its compressor runs on the calling thread and blocks it.
Who is it for?
Adopt brotli.js when you need Brotli in a browser bundle, in a workerless Node process, or in a build step where a native addon is not an option, and the extra work of compiling or bundling the encoder is acceptable. Do not adopt it to compress large payloads on a hot server path, because the README states that encoding runs on the calling thread and does not use workers, so it blocks whatever thread calls it.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 13 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two ports with two different build stories

Brotli.js is a port of the Brotli compression algorithm, as used in the WOFF2 font format, to JavaScript. The original C++ source code lives in the Google brotli repository, and the port references the algorithm specification draft as well.

The interesting detail is that the two halves were ported differently, and the README says so directly. The decompressor is hand ported, and the compressor is ported with Emscripten. That asymmetry is the whole architecture of the package, and it explains every design decision that follows.

A hand ported decompressor means the decode path is ordinary JavaScript with no toolchain, no WebAssembly and no build step. The repository layout shows it: a `dec/` directory holds the decoder and its dictionary data, and `decompress.js` and `decompress.d.ts` are the entry point. You can depend on the decompressor and never touch the compiler.

The Emscripten port is the other half. The compressor is the original C++ encoder compiled to WebAssembly through `emcc`, and the `Makefile` shows the whole configuration: the exported functions are `_malloc`, `_free` and `_encodeWithDictionary`, the runtime is modularized and exported as an ES module, and the linker flags include `FILESYSTEM=0` and `WASM=0`. That last pair is worth pausing on. `FILESYSTEM=0` means there is no emscripten filesystem, so the encoder has no way to read files, and `WASM=0` means the output is plain JavaScript rather than a WebAssembly binary, which is what lets the package work as a plain ESM import with no `.wasm` file to serve.

The initial memory size in the linker flags is set to a fixed value, and `EXPORT_KEEPALIVE=1` is present, which are the two details that stop the WebAssembly module from being garbage collected or from running out of memory on a large input.

So the trade is explicit. Decoding is free of ceremony. Encoding costs you an Emscripten build, and the package takes that cost on itself by shipping the generated encoder.

The ESM-only package and its four entry points

Brotli.js is an ESM-only package, and the package manifest makes that concrete. The type is `module`, the main entry is `index.js` with types from `index.d.ts`, and the exports map declares four subpaths: the root, `./compress`, `./compress.js`, `./decompress` and `./decompress.js`. Each one pairs a types file with a default JavaScript file, so the type declarations travel with the module for every entry point.

The public entry points have no default exports. The named functions are imported directly.

javascript
import { compress, decompress } from 'brotli';

The two standalone subpaths exist for a specific reason, stated in the README: importing either function on its own is useful for browser builds, and the standalone modules export their named function. Both subpaths also accept a `.js` extension.

javascript
import { decompress } from 'brotli/decompress';
import { compress } from 'brotli/compress';

The payoff is stated plainly: importing only `brotli/decompress` does not load the encoder. For a browser bundle, that is the difference between shipping the decoder and shipping the decoder plus an Emscripten generated encoder module.

There is one internal detail behind that, and it shows how carefully the package is built for bundlers. The `#dictionary-data` import is a package import condition: it uses the inline dictionary under the `node` condition and the compressed dictionary under `default`, which includes browser builds. So the same source resolves to a Node-friendly inline dictionary in Node and to a smaller compressed one in a browser bundle, with no configuration from the user. Browser bundlers must support ESM and conditional `package.json` imports, and the browser tests target ESM and ES2022.

On the CommonJS side, modern Node.js can load these ES modules with `require()` without a flag: Node 20.19+, 22.12+ and later releases support the Node interoperability feature.

javascript
const { compress, decompress } = require('brotli');

compress(Buffer.from('Hello, Brotli!')).then(compressed => {
  const original = decompress(compressed);
  console.log(Buffer.from(original).toString());
}).catch(console.error);

On older Node.js versions, CommonJS applications can use dynamic `import()` instead.

compress() is lazy, async and single threaded

The API surface is two functions, and they behave very differently from each other.

`decompress(buffer, [outSize])` is synchronous. It decompresses the given buffer to produce the original input to the compressor. The `outSize` parameter is optional and will be computed by the decompressor if not provided, and the README notes that inside a WOFF2 file this can be computed from the WOFF2 directory.

javascript
import { readFileSync } from 'node:fs';
import { decompress } from 'brotli';

// decode a buffer where the output size is known
decompress(compressedData, uncompressedLength);

// decode a buffer where the output size is not known
decompress(readFileSync('compressed.bin'));

`compress(buffer, options)` returns a `Promise<Uint8Array | null>`. The Promise resolves to `null` when the encoder reports failure, while initialization and runtime errors reject it. That distinction is worth internalising, because a `null` resolution is not an exception and a bare `await` will hand you a null you have to check.

The options object mirrors the Brotli encoder's own parameters.

javascript
import { readFileSync } from 'node:fs';
import { compress } from 'brotli';

// encode a buffer of binary data
const compressed = await compress(readFileSync('myfile.bin'));

// encode some data with options (default options shown)
const compressedWithOptions = await compress(readFileSync('myfile.bin'), {
  mode: 0, // 0 = generic, 1 = text, 2 = font (WOFF2)
  quality: 11, // 0 - 11
  lgwin: 22, // window size
  dictionary: ''
});

The `mode` value of 2 is the font mode, which is the WOFF2 path this port was written for.

The asynchronous behaviour has a specific shape. `compress()` returns a Promise and initializes the encoder lazily on its first call. Concurrent calls share the same initialization Promise, and a failed initialization is not retried. No startup timer or initialization call is needed, and the package does not use top-level await.

The last sentence is the limitation to plan around. Encoding runs on the calling thread after initialization, this API does not use workers or move CPU work off that thread, and input and options are not copied before initialization, so you have to keep the input buffer, options and dictionary unchanged until the Promise settles.

Building the encoder yourself with emsdk

Building the repository requires Git, Make, Python 3, Node.js and the Emscripten SDK, and the README is explicit that the `emcc` compiler is provided by Emscripten and not by npm.

The build sequence from the repository root initializes the Brotli sources as a submodule and installs a local SDK.

sh
git submodule update --init --recursive
git clone https://github.com/emscripten-core/emsdk.git .emsdk
./.emsdk/emsdk install 6.0.12
./.emsdk/emsdk activate 6.0.12
npm ci
npm test

The pinned Emscripten version is 6.0.12, and the SDK is cloned into the repository rather than installed globally. The local SDK is excluded from Git and npm packages, and the Makefile automatically uses the compiler in `.emsdk`.

`npm ci` runs `make` through the `prepublish` script and generates `build/encode.js` as an ES module. The `prepublish` script in the manifest is literally `make`, and the test script runs mocha through Node's experimental VM modules flag, `node --experimental-vm-modules node_modules/mocha/bin/mocha.js`.

If Emscripten is already installed elsewhere, you can activate its environment with `source /path/to/emsdk/emsdk_env.sh` before installing dependencies, and you can select a compiler explicitly with `make EMCC=/path/to/emcc`. The Makefile's `EMCC` variable defaults to the compiler in `.emsdk/upstream/emscripten/emcc` if that file exists and to plain `emcc` otherwise, so the two paths compose. Running `make` rebuilds the encoder and `make clean` removes generated files, and the `clean` target removes both the object files and the `build/` directory.

The Makefile also has a `check-build-tools` target, and it is worth reading what it checks. It verifies that the compiler is on the path and that `vendor/brotli/c/include/brotli/encode.h` exists, printing a specific message telling you to run the submodule update if the headers are missing. The vendored sources under `vendor/brotli/c/` are the actual Google Brotli C sources, and the compile step assembles `compress.c` with every file from the `common` and `enc` directories.

The overhead the Emscripten port carries

The linker flags in the Makefile are where the cost of the port becomes concrete, and they explain the shape of the API as well.

`MINIMAL_RUNTIME=1` keeps the emscripten runtime as small as possible, and `EXPORTED_RUNTIME_METHODS="['HEAPU8']"` exports only one runtime method, the typed array view of the WebAssembly heap. That single export is the entire interface between the JavaScript wrapper and the compiled encoder: the wrapper writes bytes into the heap through `HEAPU8`, calls `_encodeWithDictionary`, and reads the result back out.

`_malloc` and `_free` are the other two exported functions, and they are there because the wrapper has to allocate and release the buffer that the encoder writes into. The `EXPORT_ES6=1` and `MODULARIZE=1` flags are what make the output an ES module rather than a global script, and `EXPORT_KEEPALIVE=1` stops the module from being collected while the WebAssembly instance is still in use.

`INITIAL_MEMORY=318767104` sets the initial heap to a fixed size, roughly 300 megabytes of address space reserved for the module. That is a large fixed allocation for a JavaScript package, and it is the price of compiling the encoder to plain JavaScript rather than shipping a WebAssembly binary. The `--closure 1` flag runs the closure compiler over the output, which shrinks it.

The trade-off the project is making is legible from these flags. The output is a self contained ES module with no sidecar `.wasm` file, no filesystem and no worker, which makes it trivially importable from a bundler and from Node. What it costs is a large generated file, a fixed memory reservation, and the fact that the encoder's work happens on the thread that called it.

That last point is the one that decides whether the library is appropriate. The README is explicit that the API does not use workers or move CPU work off that thread. A browser that compresses a few kilobytes during a build step is fine. A Node service that compresses a request body on the event loop is going to feel it, and the alternative is to hand the work to a worker thread yourself, which the API is not designed to do.

Licence, versioning and the shape of the package

The README states the licence as MIT, and the package manifest carries the MIT field with the author listed as Devon Govett. The repository's own licence field is recorded as unknown, so the LICENSE file and the manifest are the two places to check, and the manifest is the more explicit of them.

The version in the manifest is 2.0.0, and the type is `module`. The package has a single runtime dependency, `base64-js`, which is used for the dictionary data, and the devDependencies are esbuild for the browser build and mocha for the tests. The `files` field ships only `lib` and `index.js`, so the vendored C sources, the `dec/` directory internals beyond the entry points, and the build artefacts stay out of the tarball.

The package names are worth noting because they are easy to get wrong. The npm package is named `brotli`, not `brotli.js`, and the repository is `foliojs/brotli.js` while the manifest's repository field points at `devongovett/brotli.js`. The homepage in the manifest is the same older location. The project moved, and the package metadata still carries the old URLs, which is a small trap if you are filing an issue or reading a stack trace from a published build.

The repository also has a `CHANGELOG.md` and no GitHub releases, so the changelog is the version history, and the npm package version is the release identifier. The last push to the tree was on 2026-10-10, the repository is not archived, and the difference between this package and a native Brotli binding is not the algorithm but the build system: the same C++ encoder, reached through a generated ES module rather than through a shared library.

Editorial conclusion

Adopt brotli.js when you need Brotli in a browser bundle, in a workerless Node process, or in a build step where a native addon is not an option, and the extra work of compiling or bundling the encoder is acceptable. Do not adopt it to compress large payloads on a hot server path, because the README states that encoding runs on the calling thread and does not use workers, so it blocks whatever thread calls it. Before committing, measure the encoder's cost on your own input sizes, and if you only need to read Brotli, import `brotli/decompress` alone, since the README states that importing only the decompress entry point does not load the encoder.

Frequently asked questions

How do I install brotli.js and import it?

Install it with `npm install brotli`. It is an ESM-only package with no default exports, so you import the named functions with `import { compress, decompress } from 'brotli'`. Modern Node.js can also load it with `require()` on Node 20.19+ and 22.12+.

Can I import only the decompressor in a browser bundle?

Yes. The package exposes `brotli/decompress` and `brotli/compress` as separate entry points, each exporting its named function, and the README states that importing only `brotli/decompress` does not load the encoder. Browser bundlers must support ESM and conditional `package.json` imports.

Does brotli.js compress in a worker thread?

No. The README states that encoding runs on the calling thread after initialization, and that the API does not use workers or move CPU work off that thread. It also notes that input and options are not copied before initialization, so they must stay unchanged until the Promise settles.

What do I need to build brotli.js from source?

Git, Make, Python 3, Node.js and the Emscripten SDK, with the `emcc` compiler provided by Emscripten rather than npm. The build runs `git submodule update --init --recursive`, clones emsdk into `.emsdk`, installs and activates version 6.0.12, then runs `npm ci`, which triggers `make` through the prepublish script.

Official sources

  1. foliojs/brotli.js on GitHub
  2. Issues
  3. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/foliojs-brotli-js.svg)](https://hysenlabs.com/projects/foliojs-brotli-js)

Was this useful?