@napi-rs/lzma
lzma / lzma2 / xz compression for Node.js and the browser, backed by the pure-Rust lzma-rust2 crate via napi-rs.
Help me to become a full-time open-source developer by sponsoring me on Github
Install
yarn add @napi-rs/lzma
Support matrix
Full matrix as text
Node.js
engines.node is ^22.20 || ^24.12 || >=25 — a deliberately non-contiguous range:
| Range | Supported | Note |
|---|---|---|
< 22.20 |
no | |
22.20 – 22.x |
yes | tested in CI |
23.x |
no | reached end-of-life 2025-06-01 |
24.0 – 24.11 |
no | |
24.12 – 24.x |
yes | tested in CI |
25.x |
yes | permitted, but reached end-of-life 2026-06-01 |
26 and later |
yes | not in the CI matrix |
Release-line status as of 2026-07-10: 22 is Maintenance LTS, 24 is Active LTS, 26 is Current.
Why these exact cutoffs? They are a support policy, not a technical limit. The only hard
floor in the shipped code is Node 22.12, where require(esm) became unflagged — main.js
loads stream-polyfill.mjs with require. The native binding itself asks for nothing newer
than Node-API 5. The ^22.20 || ^24.12 || >=25 range was inherited from the test toolchain
(ava declares ^22.20 || ^24.12 || >=26) and predates the require(esm) code by two months.
Node 23 and 24.0–24.11 are dropped by policy; the code runs on them.
Targets
| Rust triple | Platform | CI |
|---|---|---|
x86_64-pc-windows-msvc |
Windows x64 | tested — node 22, 24 |
aarch64-pc-windows-msvc |
Windows arm64 | tested — node 22, 24 |
i686-pc-windows-msvc |
Windows x32 | tested — node 22 (x86), --serial |
x86_64-apple-darwin |
macOS x64 | tested — node 22, 24 |
aarch64-apple-darwin |
macOS arm64 | tested — node 22, 24 |
x86_64-unknown-linux-gnu |
Linux x64 gnu | tested — node 22, 24 |
x86_64-unknown-linux-musl |
Linux x64 musl | tested — node 22, 24 |
aarch64-unknown-linux-gnu |
Linux arm64 gnu | tested — node 22, 24 |
aarch64-unknown-linux-musl |
Linux arm64 musl | tested — node 22, 24 |
armv7-unknown-linux-gnueabihf |
Linux armv7 gnu | tested — node 22 only, --serial |
x86_64-unknown-freebsd |
FreeBSD x64 | tested — node version unpinned |
powerpc64le-unknown-linux-gnu |
Linux ppc64le | non-blocking (continue-on-error) |
s390x-unknown-linux-gnu |
Linux s390x | non-blocking (continue-on-error) |
riscv64gc-unknown-linux-gnu |
Linux riscv64 | built, not tested |
aarch64-linux-android |
Android arm64 | built, not tested |
arm-linux-androideabi |
Android armv7 | built, not tested |
wasm32-wasi-preview1-threads |
wasm32-wasi, browser | built, not tested |
Seventeen targets: eleven CI-tested, two non-blocking, four built but not exercised.
Browser
Bundlers resolve @napi-rs/lzma-wasm32-wasi through the browser export condition. The wasm
build allocates shared memory and spawns worker threads, so SharedArrayBuffer must be
available — the page has to be
cross-origin isolated,
served with Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp.
API
xz
import { compress, decompress } from '@napi-rs/lzma/xz'
const compressed = await compress('Hello napi-rs 🚀')
const decompressed = await decompress(compressed)
console.log(decompressed.toString('utf8')) // Hello napi-rs 🚀
lzma
import { compress, decompress } from '@napi-rs/lzma/lzma'
const compressed = await compress('Hello napi-rs 🚀')
const decompressed = await decompress(compressed)
console.log(decompressed.toString('utf8')) // Hello napi-rs 🚀
lzma2
import { compress, decompress } from '@napi-rs/lzma/lzma2'
const compressed = await compress('Hello napi-rs 🚀')
const decompressed = await decompress(compressed)
console.log(decompressed.toString('utf8')) // Hello napi-rs 🚀
Streaming
Every namespace (xz, lzma, lzma2) additionally exposes an incremental streaming API. The one-shot compress / decompress above are unchanged; streaming is purely additive.
Incremental classes
Feed data chunk-by-chunk with update() and flush with finish(). The valid stream is the concatenation of every update() output plus the finish() tail.
import { Compressor, Decompressor } from '@napi-rs/lzma/xz'
const compressor = new Compressor({ preset: 6 })
const parts = [compressor.update('Hello '), compressor.update('napi-rs 🚀'), await compressor.finish()]
const compressed = Buffer.concat(parts)
const decompressor = new Decompressor()
const restored = Buffer.concat([decompressor.update(compressed), await decompressor.finish()])
console.log(restored.toString('utf8')) // Hello napi-rs 🚀
The top-level entry re-exports the same classes with format-qualified names: XzCompressor / XzDecompressor, LzmaCompressor / LzmaDecompressor, Lzma2Compressor / Lzma2Decompressor.
Web Streams
Each namespace exposes a WHATWG Web Streams API — Uint8Array in, compressed Uint8Array out:
import { compressStream, decompressStream } from '@napi-rs/lzma/xz'
const compressed = source.pipeThrough(new TransformStream()) // any ReadableStream<Uint8Array>
const restored = decompressStream(compressStream(source))
input must be a WHATWG ReadableStream; wrap a Node Readable with Readable.toWeb().
Node Duplex factories
For ready-to-pipe Node streams, each namespace subpath exports createCompressStream() / createDecompressStream(), which return a Node Duplex:
import { createReadStream, createWriteStream } from 'node:fs'
import { createCompressStream } from '@napi-rs/lzma/xz'
createReadStream('input.txt').pipe(createCompressStream()).pipe(createWriteStream('input.txt.xz'))
Backend & platform notes
- Backend: compression is powered by the pure-Rust
lzma-rust2crate (previouslylzma-rs). The one-shot API behavior is unchanged and the output remains standard, liblzma-compatible.xz/.lzma. - wasm / browser: the incremental classes run natively on every target. The native tokio-backed
compressStream/decompressStreamtransforms are compiled out of the wasm build, so under wasm the Web Streams API transparently falls back to a buffered polyfill (it drains the input, runs the class API, and emits a single chunk). The NodeDuplexfactories are Node-only and are not part of the browser entry. - lzma2 dictionary: raw LZMA2 carries no dictionary size in-band, so it uses a fixed 8 MiB dictionary by default. Override it via a symmetric
{ dictSize }on both the compressor and the decompressor — they must agree. A decoder configured with a smaller dictionary than the encoder fails cleanly with anInvalidArgerror; it never silently corrupts the output. - Trailing bytes: as with the one-shot API, the
lzma/lzma2stream decoders complete at the in-band end marker and ignore any trailing bytes after a complete frame; thexzdecoder validates its framed trailer.
Known limitation
Cancelling a Web compressStream / decompressStream output while a read() is still pending on a stalled or never-ending input can leak one worker thread until the process exits. Normal cancels — before reading, after data has flowed, or on any finite input — are unaffected. (Root cause: napi's stream cancel cannot interrupt a pull that is already in flight; a full fix needs an upstream napi cancel hook.)