JSON Toolkit

Перестаньте стискати JSON у неправильному порядку: мініфікація, gzip і brotli, виміряно

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

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

Коротко: спершу відділіть транспортне стиснення від структурного обрізання. Якщо gzip або brotli ще не ввімкнено, виправте конфігурацію сервера — сам gzip -6 дає 94,5% і залишає мініфікації лише 0,9 пункту. Повні 46,3% мініфікація дає лише тоді, коли рахунок байтів фіксується локально: тіла запитів, 6 МБ Lambda, 1 MiB Firestore або JSON, записаний на диск і в логи.

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

1. Розділіть «стиснення» на дві різні речі

Те, що люди називають стисненням JSON, насправді дві не пов'язані операції, і їх змішування — початок кожного неправильного висновку.

мініфікація (прибрати пробільні символи) gzip / brotli (транспортне стиснення)
Природа Перетворення документа Перетворення транспорту
Вивід Усе ще валідний, усе ще читабельний текст JSON Двійковий; непридатний, доки не декодовано
Де осідає виграш Диск, памʼять, логи, ліміти розміру Лише в мережі й у кешах
Після декодування Байти залишаються меншими Байти повертаються до оригінального розміру
Хто ним володіє Ваш скрипт збірки, аргументи серіалізатора Nginx, CDN, серверний фреймворк

Останні два рядки — ті, що важать. Ліміти платформи міряються після декодування: межа навантаження AWS Lambda в 6 МБ, межа Firestore в 1 MiB на документ. Увімкнення gzip не купує там жодного байта, бо обидва рахують декодований результат.

2. Мініфікація прибирає рівно одну річ: структурні пробільні символи

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

Це робить всю операцію одним рядком:

const minified = JSON.stringify(JSON.parse(pretty));
// RFC 8259 дозволяє пробільні символи лише довкола структурних символів,
// тож пробіли всередині рядка — це дані, а не форматування
JSON.stringify(JSON.parse('{"note": "two spaces"}'));
// {"note":"two spaces"} — обидва внутрішні пробіли збережено

Варто назвати три операції, які зараховують до «стиснення», але насправді змінюють документ: відкидання ключів зі значенням null, округлення дробових чисел і перевпорядкування ключів. Вони змінюють документ, а не переформатовують його, тож будь-який інструмент, що рекламує «глибоке стиснення», потребує благословення вашого споживача, перш ніж ви його ввімкнете.

Оскільки мініфікація прибирає лише пробільні символи, її виплата залежить майже цілком від того, яка частина вашого файлу — це відступи. Очікуйте, що це раз за разом виринатиме в таблицях нижче.

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 пробіли) після мініфікації мініфікація економить
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%», і він не бреше. Проблема з'являється на кроці другому.

Крок другий: стисніть кожен варіант, потім запитайте, скільки мініфікація все ще додає.

Зразок поверх 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. Масив логів іде просто в мінус: мініфікувати-потім-стиснути трохи більший за стиснення гарного оригіналу. Мінус — не баг, це саме те, що слід очікувати, і наступний розділ пояснює чому.

Крок третій: загальне скорочення проти оригінального гарного документа — число, що фактично подорожує мережею.

Зразок лише gzip -6 мініфікація + gzip -6 мініфікація + 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 з'їдає заслугу мініфікації

gzip — це LZ77, за яким іде кодування Гаффмана. Основний хід LZ77: «озирнися на 32-кілобайтове вікно, знайди повторювану послідовність, заміни її покажчиком (відстань, довжина)». Відступи — найлегша можлива ціль для цього ходу: кожен рівень вкладеності видає ідентичний ряд пробілів, а десятки сусідніх рядків повторюють ту саму комбінацію. Гаффман тоді призначає короткі коди високочастотним символам, тож шаблон на кшталт \n плюс вісім пробілів — той, що може повторюватися тисячу разів в одному файлі — зрештою коштує два-три біти.

Тож байти, які видаляє мініфікація, — це саме ті байти, які стиснювач згорнув би майже безкоштовно. Робити це самому означає повторювати роботу, у якій стиснювач уже чудовий, і ваші 46% згортаються до 17%. Ось уся історія за таблицею вище.

Негативний результат масиву логів вартий окремого пояснення. Той зразок майже цілком складається з довгих рядків (рядки логів, URL, base64), тож мало повторів і висока ентропія — мало надлишковості, яку стиснювач узагалі міг би прибрати. Переведення рядків у гарній версії випадково дають зіставлячувачу стабільні межі. Приберіть їх — і LZ77 втрачає ці якорі: у цьому зразку стиснення гарного оригіналу на gzip -1 дає 1 508 байтів, тоді як мініфікувати-потім-стиснути дає 1 518 байтів. На десять байтів гірше.

Впорядковані за величиною, важелі шикуються так:

Важіль Заощаджено проти оригінального гарного документа (вкладені замовлення) Ціна
мініфікація 46,3% (лише сирі байти) Читабельність зникає; потрібен крок збірки
gzip -6 94,5% Трохи CPU сервера; майже нульова конфігурація
мініфікація + gzip -6 95,4% Два вище, додані разом
мініфікація + brotli 96,9% Потрібен HTTPS і модуль brotli; попередньо стискайте статичні ресурси

Стрибок від 0 до 94,5% — робота gzip. Стрибок від 94,5% до 95,4% — дев'ять десятих пункту — робота мініфікації. Ця таблиця вирішує, де належать ваші зусилля.

5. Рівень стиснення — прихована змінна

Будь-який аргумент, що припускає єдиний фіксований рівень gzip, стоїть на піску, бо рівень прямо змінює граничну цінність мініфікації:

Налаштування транспорту Додаткові байти, які мініфікація все ще економить (вкладені замовлення)
gzip -1 (типове gzip_comp_level у Nginx) 21,0%
gzip -6 (типове в більшості стандартних бібліотек) 17,0%
gzip -9 8,9%
brotli (типова якість) 10,6%

Що слабше ваше транспортне стиснення, то більше виплачує мініфікація. Це дає практичне правило: на стандартному встановленні Nginx, де gzip_comp_level усе ще 1, мініфікація дає вам 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

Три числа відповідають одночасно на три питання: чи стиснення взагалі ввімкнене, на якому рівні й скільки місця залишилося для мініфікації. Якщо identity і gzip повертаються майже ідентичними, стиснення зовсім не активне — і виправляти треба Nginx, а не ваш JSON:

gzip on;
gzip_comp_level 6;
gzip_min_length 256;      # пропускайте крихітні відповіді, вони лише зростають
gzip_types application/json application/javascript text/css;

6. Коли мініфікація справді виплачує

Це вирішує один тест: чи байти, які ви заощадили, будуть десь фіксуватися, чи ви натрапите на ліміт, що рахує сирі байти? Якщо так, повні 46% осідають. Якщо відповідь «вони існують лише в мережі», то 17% — їхня справжня цінність.

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

По-друге, канали з жорсткою стелею байтів — і кожен із них рахує декодовані байти:

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

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

По-третє, JSON, записаний на диск або в логи. Логи й індекси зберігають буквальні байти без залучення транспортного шару. Є застереження, що збиває людей: мініфікація допомагає лише якщо ви писали гарний 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 — інструменти diff працюють посторінково, тож однорядковий 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% швидше. Половинний розмір не дає половинного часу, бо парсер витрачає більшу частину бюджету на нарізання рядків і перетворення чисел; пропуск пробільних символів — побічний. Що фактично визначає ціну розбору — кількість ключів і вміст рядків, що детально розбирає Розбір, відображення й потокова обробка великих файлів JSON.

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

8. Таблиця рішень: який крок перший

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

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

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

Чи мініфікований JSON усе ще валідний JSON?

Так. Мініфікація прибирає лише пробільні символи, які 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% дісталася мережі.

Чи комітити конфігураційні файли мініфікованими?

Ні. Інструменти diff порівнюють рядок за рядком, тож однорядковий JSON перетворює кожну редакцію на «змінився весь рядок», а розвʼязання конфліктів коштує набагато більше за заощаджені байти. Тримайте гарні версії в репозиторії й мініфікуйте під час збірки.

Наскільки великою має бути відповідь, щоб gzip був вартий?

Звичайний поріг — 256 байтів до 1 КБ. Відповідь така мала, як {"ok": true} — 16 байтів з відступами — зростає до 36 байтів під gzip -6, бо словник і заголовки коштують більше, ніж відновлюють. Саме це контролює gzip_min_length.