JSON Toolkit

Прекратите сжимать JSON в неправильном порядке: minify vs gzip vs brotli, измеренные

Главное: Минификация срезает 30–50% с сырого JSON, но gzip стирает большую часть этой выгоды. Измеренные числа по реальным формам, плюс четыре случая, когда минификация делает хуже.

Средний~7 мин чтенияОпубликовано 2026-10-31Обновлено 2026-10-31
Сжатие JSON: измеренное сравнение размера полезной нагрузки minify, gzip и brotli

Главное: сначала отделите транспортное сжатие от структурной обрезки. Если gzip или brotli ещё не включены, исправьте конфигурацию сервера — один gzip -6 достигает 94.5% и оставляет minify стоимостью всего в 0.9 пункта. Полные 46.3% от minify реализуются только когда байтовый счёт записывается локально: тела запросов, 6 МБ Lambda, 1 МиБ Firestore или JSON, записанный на диск и в логи.

Значок «экономия 35%» на вашем любимом минификаторе честен — он измеряет сырые байты. Ваш API работает поверх gzip или brotli, и эти два съедают ровно ту же избыточность. Перемножьте два числа, и вы получите неверный ответ.

1. Разделите «сжатие» на две разные вещи

То, что люди называют сжатием JSON, на самом деле две несвязанные операции, и их смешение — начало всех неверных выводов.

minify (удаление пробелов) gzip / brotli (транспортное сжатие)
Природа Преобразование документа Преобразование транспорта
Вывод Всё ещё валидный, читаемый текст JSON Двоичный; непригоден до декодирования
Куда попадает выигрыш Диск, память, логи, лимиты размера Только в передаче и кешах
После декодирования Байты остаются меньше Байты возвращаются к исходному размеру
Кто владеет Ваш скрипт сборки, аргументы сериализатора Nginx, CDN, серверный фреймворк

Последние две строки — те, что важны. Лимиты платформ измеряются после декодирования: ограничение полезной нагрузки Lambda в 6 МБ, ограничение Firestore в 1 МиБ на документ. Включение gzip не покупает там ни одного байта, потому что оба считают декодированный результат.

2. minify удаляет ровно одну вещь: структурные пробелы

RFC 8259 допускает пробелы (space, \t, \n, \r) вокруг любого структурного символа. Ничто другое в документе JSON не является необязательным: имена ключей — данные, числа сохраняют каждую цифру, а содержимое строк побайтово неприкосновенно.

Это делает всю операцию одной строкой:

const minified = JSON.stringify(JSON.parse(pretty));
// RFC 8259 allows whitespace only around structural characters,
// so spaces inside a string are data, not formatting
JSON.stringify(JSON.parse('{"note": "two spaces"}'));
// {"note":"two spaces"} - both inner spaces are preserved

Стоит отметить три операции, которые относят к «сжатию», но на самом деле изменяют документ: отбрасывание ключей со значением null, округление чисел с плавающей точкой и переупорядочивание ключей. Они меняют документ, а не переформатируют его, поэтому любой инструмент, рекламирующий «глубокое сжатие», нуждается в благословении вашего потребителя, прежде чем вы его включите.

Поскольку minify удаляет только пробелы, его выигрыш почти полностью зависит от того, насколько ваш файл состоит из отступов. Ожидайте, что это будет всплывать снова и снова в таблицах ниже.

3. Измерение: четыре документа, три уровня gzip плюс brotli

Окружение: Node v22.13.1, zlib.gzipSync на уровне 1 / 6 / 9, brotliCompressSync на качестве по умолчанию (11). Четыре образца намеренно покрывают формы, которые вы реально отправляете: 50 вложенных заказов (глубокая структура, повторяющиеся ключи), конфигурация с 6 уровнями (длинные ключи, короткие значения), массив логов из 200 строк (длинные строки, почти без отступов) и реальный media-manifest.json с этого сайта (смешанная форма, строки с обилием URL).

Шаг первый: сырые байты.

Образец pretty (2 пробела) pretty (4 пробела) после minify minify экономит
50 вложенных заказов 29,551 B 40,761 B 15,881 B 46.3%
Конфигурация с 6 уровнями 25,450 B 38,012 B 11,679 B 54.1%
Массив логов из 200 строк 14,582 B 14,982 B 13,981 B 4.1%
media-manifest.json 25,149 B 28,114 B 21,222 B 15.6%

Вот откуда берётся значок «экономия 35%», и он не лжёт. Проблема появляется на шаге два.

Шаг второй: сжать каждый вариант, затем спросить, сколько minify всё ещё добавляет.

Образец поверх gzip -1 поверх gzip -6 поверх gzip -9 поверх brotli
50 вложенных заказов 21.0% 17.0% 8.9% 10.6%
Конфигурация с 6 уровнями 74.2% 59.1% 58.0% 56.4%
Массив логов из 200 строк -0.7% -0.4% 0.6% -0.3%
media-manifest.json 1.5% 1.0% 1.0% 1.6%

Тот же документ вложенных заказов, который теряет 46.3% сырых байтов, сохраняет только 17.0% от этого, когда в игре gzip -6. Массив логов уходит прямо в минус: minify-затем-сжать немного больше, чем сжатие красивого оригинала. Минус — не баг, это ровно то, что следует ожидать, и следующий раздел объясняет почему.

Шаг третий: общее сокращение относительно исходного красивого документа — число, которое реально путешествует по проводу.

Образец только gzip -6 minify + gzip -6 minify + brotli
50 вложенных заказов 94.5% 95.4% 96.9%
Конфигурация с 6 уровнями 96.0% 98.4% 99.0%
Массив логов из 200 строк 89.9% 89.9% 95.0%
media-manifest.json 83.5% 83.6% 86.1%

4. Почему gzip съедает заслугу minify

gzip — это LZ77, за которым следует кодирование Хаффмана. Основной ход LZ77 — «оглянись в 32 КБ окне, найди повторяющуюся последовательность, замени её указателем (расстояние, длина)». Отступы — самая лёгкая мишень для этого хода: каждый уровень вложенности выдаёт идентичный пробежный прогон, и десятки соседних строк повторяют одну и ту же комбинацию. Затем Хаффман назначает короткие коды высокочастотным символам, поэтому шаблон вроде \n плюс восемь пробелов — тот, что может повторяться тысячу раз в одном файле — в итоге стоит два-три бита.

Итак, байты, которые удаляет minify, — это именно те байты, которые компрессор схлопнул бы почти бесплатно. Делать это самому означает переделывать работу, в которой компрессор уже превосходен, и ваши 46% схлопываются до 17%. Вот и вся история за таблицей выше.

Отрицательный результат массива логов заслуживает отдельного объяснения. Тот образец почти целиком состоит из длинных строк (строки логов, URL, base64), что означает мало повторов и высокую энтропию — мало избыточности для компрессора с самого начала. Переводы строк в красивой версии случайно дают сопоставителю стабильные границы. Уберите их, и LZ77 теряет эти якоря: в этом образце сжатие красивого оригинала на gzip -1 даёт 1,508 байтов, тогда как minify-затем-сжать даёт 1,518 байтов. На десять байтов хуже.

По величине рычаги выстраиваются так:

Рычаг Экономия против исходного красивого документа (вложенные заказы) Цена
minify 46.3% (только сырые байты) Читаемость потеряна; нужен шаг сборки
gzip -6 94.5% Немного серверного CPU; почти нулевая конфигурация
minify + gzip -6 95.4% Два вышеперечисленных, сложенные
minify + brotli 96.9% Нужны HTTPS и модуль brotli; предварительное сжатие статических ресурсов

Скачок с 0 до 94.5% — работа gzip. Скачок с 94.5% до 95.4% — девять десятых пункта — работа minify. Эта таблица решает, где находятся ваши усилия.

5. Уровень сжатия — скрытая переменная

Любой аргумент, предполагающий единый фиксированный уровень gzip, стоит на песке, потому что уровень напрямую меняет маржинальную ценность minify:

Настройка транспорта Дополнительные байты, которые minify всё ещё экономит (вложенные заказы)
gzip -1 (по умолчанию в Nginx gzip_comp_level) 21.0%
gzip -6 (по умолчанию в большинстве стандартных библиотек) 17.0%
gzip -9 8.9%
brotli (качество по умолчанию) 10.6%

Чем слабее ваше транспортное сжатие, тем больше окупается minify. Это даёт практическое правило: на стандартной установке Nginx, где gzip_comp_level всё ещё 1, minify даёт вам 21%. Когда вы настроили до 9 или перешли на brotli, остаётся 9–11%, и обычно можно пропустить.

Прежде чем спорить в ту или иную сторону, измерьте, на каком уровне вы реально находитесь:

# Получить один и тот же ответ с тремя значениями Accept-Encoding и сравнить, что пришло
for enc in identity gzip br; do
  size=$(curl -s -o /dev/null -w '%{size_download}' \
    -H "Accept-Encoding: $enc" https://api.example.com/v1/orders)
  echo "$enc -> $size bytes"
done

Три числа сразу отвечают на три вопроса: включено ли сжатие вообще, на каком уровне, и сколько места осталось для minify. Если identity и gzip возвращаются почти идентичными, сжатие вовсе не активно — и исправлять надо Nginx, а не ваш JSON:

gzip on;
gzip_comp_level 6;
gzip_min_length 256;      # skip tiny responses, they only grow
gzip_types application/json application/javascript text/css;

6. Когда minify действительно окупается

Один тест решает: будут ли сохранённые байты записаны где-то, или вы упрётесь в лимит, считающий сырые байты? Если да, реализуются полные 46%. Если ответ «они существуют только в передаче», то 17% — их реальная ценность.

Первое, тела HTTP-запросов. Большинство серверов не распаковывают тела запросов, а gzip настроен почти исключительно для ответов. JSON, который вы POST-ите, обычно не имеет транспортной защиты вообще, поэтому экономия minify приходит по полной цене. То же касается вебхук-колбэков, телеметрии MQTT и кадров WebSocket, которые не согласовали permessage-deflate.

Второе, каналы с жёстким байтовым потолком — и каждый из них считает декодированные байты:

Канал Лимит Может ли gzip помочь?
Синхронная полезная нагрузка AWS Lambda 6 МБ Нет, измеряется после декодирования
Один документ Firestore 1 МиБ Нет
Одно сообщение Kafka (по умолчанию) 1 МБ Нет, сжатие на стороне брокера — другой слой
Квота localStorage ~5 МБ (считается в символах) Нет
URL GET-запроса Зависит от сервера, обычно 8 КБ Нет

В этой категории minify — не оптимизация, а разница между запросом, который отправляется, и тем, который падает. Встраивание JSON в строку запроса URL — самый резкий случай: URL никогда не сжимаются, и у них есть ограничение длины, поэтому сначала minify, затем решайте, нужен ли вообще base64.

Третье, JSON, записанный на диск или в логи. Логи и индексы хранят буквальные байты без какого-либо транспортного слоя. Есть оговорка, которая сбивает людей: minify помогает, только если вы изначально писали красивый JSON. Тот образец логов из 200 строк экономил всего 4.1%, потому что его строки уже были компактными, и отступы составляли почти ничего от файла. Наоборот, если ваш логгер вызывает JSON.stringify(obj, null, 2), удаление третьего аргумента — бесплатные деньги.

Последнее замечание: когда сырые байты действительно стеснены, имена ключей обычно больший рычаг, чем отступы. В media-manifest.json замена его 14 уникальных ключей на k0–k13 снижает минифицированную форму с 21,222 B до 18,887 B — ещё 11.0%. В сжатом виде это оставшееся преимущество сокращается до 2.4% под gzip -6 и 1.3% под brotli. Тот же механизм, что и всё выше: отступы и имена ключей — оба повторяющаяся структурная информация, и компрессор забирает тот же дивиденд в любом случае.

7. Четыре случая, когда это делает хуже

Не минифицируйте файлы, которые вы коммитите. Измените "qty": 2 на 3, и красивый файл покажет одну чистую строку в git diff; минифицированный файл переписывает всю строку, и при сотнях полей вы вообще не найдёте изменение. Это та же механика, что описана в Как разрешать конфликты слияния JSON — инструменты различий работают построчно, поэтому однострочный JSON превращает каждую правку в «вся строка отличается». Держите красивые файлы в репозитории и минифицируйте только в выводе сборки или при записи в хранилище.

Не сжимайте крошечные ответы. Ответ, содержащий только {"ok": true}, — 16 байтов с добавлением отступов, и 36 байтов после gzip -6 — на двадцать байтов больше, потому что собственный словарь и заголовки gzip занимают место. Это не теория; именно поэтому существует gzip_min_length. Его значение по умолчанию в Nginx в 20 байтов слишком мало для продакшена, где 256 — обычный нижний порог.

Не ожидайте более быстрого парсинга. Масштабируя тот же набор заказов до 4,000 записей, красивый документ — 2,374,378 байтов против 1,282,358 минифицированного — на 46% меньше — однако медианное время JSON.parse движется только с 5.7 мс до 5.0 мс, примерно на 12% быстрее. Уменьшение размера вдвое не уменьшает время вдвое, потому что парсер тратит большую часть бюджета на нарезку строк и преобразование чисел; пропуск пробелов — incidental. Что действительно определяет стоимость парсинга — число ключей и содержимое строк, что Парсинг, отрисовка и потоковая обработка больших JSON-файлов разбирает подробно.

Не внедряйте это задним числом в сервис, уже работающий на brotli или gzip -9. Выигрыш падает до 9–11%, а стоимость — постоянная потеря читаемости при отладке — остаётся. Проверьте вместо этого, покрывает ли gzip_types вообще application/json. Множество сайтов включает gzip и сжимает только text/html, оставляя JSON полностью несжатым; в такой ситуации minify — не шаг первый, исправление конфигурации — шаг первый.

8. Таблица решений: какой шаг первым

Ваша ситуация Сделайте это первым Где minify в рейтинге
Транспортного сжатия нет вообще Настройте Nginx gzip_types или включите авто-сжатие CDN Пропустите; девять десятых выигрыша испаряются
Работает gzip -1 (по умолчанию Nginx) Поднимите gzip_comp_level до 6 Второй, стоит 21%
Уже на brotli или gzip -9 Проаудируйте длинные ключи, добавьте пагинацию или обрежьте поля Пропустите; осталось только 9–11%
Упираетесь в Lambda 6 МБ или Firestore 1 МиБ Минифицируйте напрямую; считаются полные 46% Первый
Тела POST, вебхуки, MQTT Минифицируйте напрямую; транспорт здесь редко применяется Первый
Конфигурационные файлы, идущие в Git Держите их красивыми Только в выводе сборки
Логи, телеметрия, офлайн-дампы Удалите аргумент null, 2 в логгере Это и есть шаг первый

Правило в одном предложении: посмотрите, какой байтовый реестр вы пытаетесь уменьшить — тот, что записывается локально (диск, квота, лимит платформы, тело запроса), или тот, что существует только в передаче. Локальный первым означает minify; только-передача означает сначала включите brotli или gzip.

На практике запустите цикл curl выше, чтобы подтвердить, что транспортное сжатие живо, затем прогоните образец через инструмент минификации JSON и прочитайте его цифру экономии. Если это число выше 40%, а curl сообщает identity и gzip почти одинакового размера, ваше узкое место — конфигурация сервера, а не JSON. Если два числа curl уже близки, этому документу мало что осталось дать, и вы должны вместо этого обрезать поля или пагинировать — тема Основ проектирования контрактов JSON API.

Минифицированный JSON всё ещё валиден?

Да. minify удаляет только пробелы, которые RFC 8259 допускает вокруг структурных символов, и никогда не трогает содержимое строк, поэтому JSON.parse даёт значение, идентичное оригиналу. Единственная жертва — человеческая читаемость.

brotli или gzip?

На одном и том же минифицированном образце brotli экономит 96.9%, а gzip -6 — 95.4% — узкий разрыв. Используйте gzip -6 для динамических ответов, чтобы держать CPU низким, и приберегайте brotli для статических ресурсов, доставляемых по HTTPS, которые можно предварительно сжать в файлы .br.

Я минифицировал JSON, но API не стал быстрее. Почему?

Проверьте ответ через curl -sI на заголовок Content-Encoding: gzip. Многие установки Nginx по умолчанию используют gzip_types только для text/html, поэтому JSON никогда не покрывался — и менее пятой части ваших 46% когда-либо достигало провода.

Стоит ли коммитить конфигурационные файлы минифицированными?

Нет. Инструменты различий сравнивают построчно, поэтому однострочный JSON превращает каждую правку в «вся строка изменилась», а разрешение конфликтов стоит гораздо больше, чем сохранённые байты. Держите красивые версии в репозитории и минифицируйте во время сборки.

Насколько большим должен быть ответ, чтобы gzip того стоил?

Обычный порог — от 256 байтов до 1 КБ. Ответ вроде {"ok": true} — 16 байтов с отступами — вырастает до 36 байтов под gzip -6, потому что словарь и заголовки стоят больше, чем восстанавливают. Именно это контролирует gzip_min_length.