Сжатие данных в Node.js: zlib, gzip и brotli
✓ Все примеры выполнены на Node.js v22.23.1 (LTS), июль 2026
Содержание
Сжатие в Node не требует зависимостей: модуль node:zlib встроен в платформу и умеет три формата. Вопрос не в том, как вызвать функцию, — вопрос в том, какой формат выбрать и какой ценой.
Три формата и когда какой
import { gzip, brotliCompress, deflate } from 'node:zlib';
import { promisify } from 'node:util';
const gzipAsync = promisify(gzip);
const brotliAsync = promisify(brotliCompress);
| Формат | Кто понимает | Когда брать |
|---|---|---|
| gzip | все браузеры, gunzip, любой прокси |
универсальный выбор, динамика |
| brotli | все современные браузеры | статика, сжатая заранее |
| deflate | тот же алгоритм, но без заголовков gzip | почти никогда |
Deflate — источник путаницы. Алгоритм внутри у него и у gzip один и тот же; различаются только обёртка и контрольная сумма. Gzip добавляет около 18 байт метаданных, зато его понимает командная строка. Отдельно выбирать deflate почти никогда не нужно.
Замер на реальных данных
Синтетика вроде «одна строка × 100000» показывает сжатие в 300 раз и не говорит вообще ни о чём. Возьмём реальные данные — исходник этой самой статьи, 19 947 байт текста с кодом:
const real = await readFile('node-js-promise.md');
const gz = await gzipAsync(real);
const br = await brotliAsync(real);
const df = await deflateAsync(real);
исходник: статья блога, 19947 байт
gzip : 6903 байт | 2.89x | 2.5 мс
brotli : 5909 байт | 3.38x | 19.5 мс
deflate: 6891 байт | 2.89x | 1.2 мс
экономия brotli против gzip: 14.4%
Данные таблицей
| Показатель | Размер |
|---|---|
| Исходник | 19947 байт |
| gzip | 6903 байт |
| deflate | 6891 байт |
| brotli | 5909 байт |
Источник: собственный замер, Node.js v22.23.1
Что здесь видно:
- Реальное сжатие текста — около 3x, а не 300x. Это честный ориентир для HTML, JSON и логов.
- Brotli выигрывает у gzip 14.4% — заметно, но не радикально.
- Deflate и gzip дали почти одно и то же: разница 12 байт из 6900 — те самые заголовки.
Главная ловушка brotli: качество по умолчанию
Вот цифра, ради которой стоило замерять. У gzip уровень сжатия по умолчанию средний, а у brotli — максимальный, 11. Отсюда те самые 19.5 мс.
Поменяем качество:
import { constants } from 'node:zlib';
await brotliAsync(data, { params: { [constants.BROTLI_PARAM_QUALITY]: 4 } });
brotli q=4 : 7197 байт | 2.77x | 0.9 мс
brotli q=11 : 5909 байт | 3.38x | 19.4 мс <- дефолт
gzip : 6903 байт | 2.89x | 2.5 мс
Данные таблицей
| Показатель | Время |
|---|---|
| brotli q=4 | 0.9 мс |
| deflate | 1.2 мс |
| gzip | 2.5 мс |
| brotli q=11 (дефолт) | 19.4 мс |
Источник: собственный замер, Node.js v22.23.1
Практический вывод: brotli с настройками по умолчанию нельзя ставить на динамические ответы. 19 мс процессорного времени на каждый ответ — это не «чуть медленнее», это заблокированный событийный цикл, потому что сжатие целиком считает CPU. При сотне запросов в секунду сервер просто ляжет.
Где brotli уместен: статика, сжатая один раз при сборке. Там 19 мс платятся однажды, а 14% экономии получают все посетители.
Сжатие маленьких данных вредит
const tiny = Buffer.from('ok');
const tinyGz = await gzipAsync(tiny);
исходник: 2 байт -> gzip: 22 байт
стало БОЛЬШЕ в 11x — это заголовки формата
У любого формата есть служебный заголовок. Пока данных мало, он весит больше самих данных. Отсюда правило: не сжимайте ничего меньше примерно килобайта — именно поэтому у nginx есть настройка gzip_min_length.
То же касается уже сжатого: JPEG, PNG, MP4, архивы. Прогонять их через gzip — потратить процессор и не выиграть ничего.
Правильный конвейер: pipeline
Для файлов сжатие делают потоком — тогда файл любого размера не попадает в память целиком:
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
await pipeline(
createReadStream('big.log'),
createGzip(),
createWriteStream('big.log.gz')
);
Именно pipeline, а не pipe: при ошибке он закроет всю цепочку и отдаст исключение. Подробнее — в разборе потоков.
Проверка целостности
Полезная привычка при работе со сжатием — убедиться, что распаковка возвращает исходник байт в байт:
const back = await gunzipAsync(gz);
console.log('вернул байт в байт:', back.equals(real));
gunzip вернул байт в байт : true
brotli вернул байт в байт : true
Оба формата — сжатие без потерь, и Buffer.equals это подтверждает. Если вдруг не совпало — ищите, где данные прошли через строку: конвертация в текст и обратно ломает байты, не являющиеся валидным UTF-8.
Нужно ли сжимать в Node вообще
Честный ответ: чаще всего нет. Если перед приложением стоит nginx, Caddy или облачный прокси, сжатие лучше отдать им. Причина не в удобстве, а в архитектуре: сжатие целиком нагружает процессор, а у Node один поток на ваш код — каждый сжатый ответ отнимает время у всех остальных.
Модуль zlib нужен, когда:
- вы сжимаете файлы, а не ответы: архивы, выгрузки, бэкапы;
- готовите статику при сборке — вот здесь brotli q=11 на своём месте;
- работаете с уже сжатыми данными: читаете
.gz-логи, распаковываете чужие архивы.
Что было на этой странице в 2018 году
Первая версия вышла 8 июля 2018 года. Пример был такой:
const zlib = require('zlib');
const fs = require('fs');
const gzip = zlib.createGzip();
const inp = fs.createReadStream('test.png');
var out = fs.createWriteStream('test.png.gz');
inp.pipe(gzip).pipe(out);
Три приметы времени: require, var и цепочка .pipe().pipe() — та самая, что теряет ошибки и оставляет потоки открытыми. pipeline в Node тогда ещё не завезли.
Но интереснее то, чего в статье нет: ни слова про brotli. И это не упущение автора — brotli в Node на тот момент не существовало, он появился в платформе позже. Статья разбирала gzip и deflate просто потому, что других вариантов не было. Сегодня выбор из двух форматов сжатия — и половина этого разбора посвящена тому, чего в 2018-м не могло быть в принципе.
И ещё одна деталь: пример сжимал test.png. Сегодня это как раз пример того, как делать не надо — PNG уже сжат, и gzip поверх него не даст ничего.
Смежные темы: потоки и pipeline — как сжимать файлы любого размера; HTTP-сервер — где сжатие отдают прокси; модуль fs — чтение и запись файлов. Полный список — в справочнике по Node.js.
Частые вопросы
Что выбрать для HTTP-ответов — gzip или brotli?
Нужно ли сжимать в Node, если впереди nginx или Cloudflare?
Чем deflate отличается от gzip?
gunzip; чистый deflate — нет. На нашем замере разница в размере составила 12 байт из ~6900.