node:zlib exposes four algorithms — deflate, gzip, Brotli and Zstandard — each as a Transform stream (createGzip, createBrotliCompress, createZstdCompress, plus decompressors) and as one-shot functions (gzipSync, brotliDecompress). Stream what you are already piping; the one-shot form suits payloads small enough to hold twice in memory. The Zstd classes are labeled Stability 1 – Experimental in the Node documentation; gzip, deflate and Brotli are stable.
import { createBrotliCompress, constants } from 'node:zlib';
const params = { [constants.BROTLI_PARAM_QUALITY]: 5,
[constants.BROTLI_PARAM_SIZE_HINT]: statSync('orders.csv').size };
await pipeline(
createReadStream('orders.csv'),
createBrotliCompress({ params }),
createWriteStream('orders.csv.br'),
);
console.log(`orders.csv.br ${(statSync('orders.csv.br').size / 1048576).toFixed(2)} MB`);orders.csv.br 6.24 MB
BROTLI_PARAM_SIZE_HINT tells the encoder how much data to expect so it can size its window. The level matters more than the algorithm, as compressing the first 20 MB of the same CSV shows:
| Setting | Output | Ratio | Time |
|---|---|---|---|
| gzip level 1 | 4.53 MB | 4.4x | 89 ms |
| gzip level 6 (default) | 3.88 MB | 5.2x | 761 ms |
| gzip level 9 | 3.88 MB | 5.2x | 5299 ms |
| Brotli quality 5 | 1.23 MB | 16.3x | 507 ms |
| Brotli quality 11 (default) | 1.22 MB | 16.4x | 46645 ms |
| Zstd level 3 (default) | 1.90 MB | 10.5x | 70 ms |
Gzip level 9 spent seven times the CPU of level 6 for no gain, and Brotli's default quality 11 took 92 times as long as quality 5 for 1% more compression. Compress responses on the fly at quality 4-5 and leave quality 11 to a build step, where the cost is paid once. Zstd compressed twice as well as gzip in less time than gzip level 1.
In an HTTP server, negotiate with Accept-Encoding, set Content-Encoding, and never compress an already-compressed body (JPEG, MP4, .zip): you spend CPU to make it slightly larger.