Přestaňte komprimovat JSON ve špatném pořadí: minify vs gzip vs brotli, měřeno
Shrnutí: Minifikace ukrojí 30–50 % ze surového JSON, ale gzip většinu této úspory smaže. Naměřená čísla napříč skutečnými tvary plus čtyři případy, kdy minifikace věci zhorší.

Stručně: nejprve oddělte přenosovou kompresi od strukturálního ořezu. Pokud gzip nebo brotli ještě není nasazen, opravte konfiguraci serveru – samotný gzip -6 dosahuje 94,5 % a minify pak má hodnotu jen 0,9 bodu. Plných 46,3 % minify se projeví jen tam, kde se účtují bajty lokálně: těla požadavků, 6 MB v Lambda, 1 MiB ve Firestore, nebo JSON zapisovaný na disk a do logů.
Odznak „ušetřeno 35 %“ na vašem oblíbeném minifikátoru je poctivý – měří surové bajty. Vaše API běží nad gzipem nebo brotli a ty dva smažou přesně stejnou redundanci. Vynásobte obě čísla a dostanete špatnou odpověď.
1. Rozdělte „kompresi“ na dvě různé věci
To, čemu se říká komprese JSON, jsou ve skutečnosti dvě nesouvisející operace a jejich zaměňování je začátkem každého špatného závěru.
| minify (odstranění prázdných znaků) | gzip / brotli (přenosová komprese) | |
|---|---|---|
| Povaha | Transformace dokumentu | Transformace přenosu |
| Výstup | Stále platný a čitelný text JSON | Binární; nepoužitelný, dokud se nedekóduje |
| Kde se úspora projeví | Disk, paměť, logy, limity velikosti | Jen na drátě a v caches |
| Po dekódování | Bajty zůstávají menší | Bajty se vrací k původní velikosti |
| Kdo to vlastní | Váš build skript, argumenty serializátoru | Nginx, CDN, serverový framework |
Poslední dva řádky jsou ty důležité. Limity platform se měří po dekódování: limit 6 MB pro payload v AWS Lambda, limit 1 MiB na dokument ve Firestore. Zapnutí gzipu tam nekoupí ani jediný bajt, protože oba počítají dekódovaný výsledek.
2. minify odstraňuje přesně jednu věc: strukturální prázdné znaky
RFC 8259 povoluje prázdné znaky (space, \t, \n, \r) kolem jakéhokoli strukturálního znaku. Nic jiného v dokumentu JSON není nepovinné: názvy klíčů jsou data, čísla si nechávají každou číslici a obsah řetězců je bajtově nedotknutelný.
To činí celou operaci jedním řádkem:
const minified = JSON.stringify(JSON.parse(pretty));
// RFC 8259 povoluje prázdné znaky jen kolem strukturálních znaků,
// takže mezery uvnitř řetězce jsou data, nikoli formátování
JSON.stringify(JSON.parse('{"note": "two spaces"}'));
// {"note":"two spaces"} - obě vnitřní mezery zůstávají
Za zmínku stojí tři operace, které se řadí pod „kompresi“, ale ve skutečnosti dokument mění: zahazování klíčů s hodnotou null, zaokrouhlování desetinných čísel a přeřazování klíčů. Ty dokument mění, místo aby ho přeformátovaly, takže každý nástroj nabízející „hlubokou kompresi“ potřebuje požehnání vašeho konzumenta, než ji zapnete.
Protože minify odstraňuje jen prázdné znaky, jeho přínos závisí téměř výhradně na tom, jak velkou část souboru tvoří odsazení. Počítejte s tím, že se to v tabulkách níže objeví opakovaně.
3. Měření: čtyři dokumenty, tři úrovně gzipu plus brotli
Prostředí: Node v22.13.1, zlib.gzipSync na úrovni 1 / 6 / 9, brotliCompressSync v výchozí kvalitě (11). Čtyři vzorky záměrně pokrývají tvary, které skutečně posíláte: 50 vnořených objednávek (hluboká struktura, opakované klíče), 6úrovňovou konfiguraci (dlouhé klíče, krátké hodnoty), 200řádkové pole logů (dlouhé řetězce, téměř žádné odsazení) a skutečný media-manifest.json z tohoto webu (smíšený tvar, řetězce plné URL).
Krok první: surové bajty.
| Vzorek | pretty (2 mezery) | pretty (4 mezery) | po minify | minify ušetří |
|---|---|---|---|---|
| 50 vnořených objednávek | 29 551 B | 40 761 B | 15 881 B | 46,3 % |
| 6úrovňová konfigurace | 25 450 B | 38 012 B | 11 679 B | 54,1 % |
| 200řádkové pole logů | 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 % |
Odtud pochází odznak „ušetřeno 35 %“ a nelže. Problém se objeví ve druhém kroku.
Krok druhý: zkomprimujte každou variantu a poté se ptejte, kolik minify ještě přidá.
| Vzorek | nad gzip -1 | nad gzip -6 | nad gzip -9 | nad brotli |
|---|---|---|---|---|
| 50 vnořených objednávek | 21,0 % | 17,0 % | 8,9 % | 10,6 % |
| 6úrovňová konfigurace | 74,2 % | 59,1 % | 58,0 % | 56,4 % |
| 200řádkové pole logů | -0,7 % | -0,4 % | 0,6 % | -0,3 % |
| media-manifest.json | 1,5 % | 1,0 % | 1,0 % | 1,6 % |
Stejný dokument vnořených objednávek, který ztrácí 46,3 % surových bajtů, si při gzip -6 ponechá jen 17,0 % z toho. Pole logů jde dokonce do záporu: minifikace a poté komprese je mírně větší než komprese pěkných původních dat. Záporné číslo není chyba, je přesně to, co máte očekávat, a další část vysvětluje proč.
Krok třetí: celkové zmenšení proti původnímu pěknému dokumentu – číslo, které skutečně cestuje po drátě.
| Vzorek | jen gzip -6 | minify + gzip -6 | minify + brotli |
|---|---|---|---|
| 50 vnořených objednávek | 94,5 % | 95,4 % | 96,9 % |
| 6úrovňová konfigurace | 96,0 % | 98,4 % | 99,0 % |
| 200řádkové pole logů | 89,9 % | 89,9 % | 95,0 % |
| media-manifest.json | 83,5 % | 83,6 % | 86,1 % |
4. Proč gzip pohlcuje zásluhu minify
gzip je LZ77 následované Huffmanovým kódováním. Základní tah LZ77 je „podívej se zpět do 32KB okna, najdi opakovanou posloupnost, nahraď ji ukazatelem (vzdálenost, délka).“ Odsazení je pro tento tah nejsnazším cíllem: každá úroveň vnoření vydá stejnou posloupnost mezer a desítky sousedních řádků opakují stejnou kombinaci. Huffman poté přiřadí krátké kódy symbolům s vysokou frekvencí, takže vzor jako \n plus osm mezer – který se v jediném souboru může opakovat tisíckrát – nakonec stojí dva nebo tři bity.
Bajty, které minify smaže, jsou tedy přesně bajty, které by kompresor slisoval téměř zdarma. Dělat to sám znamená opakovat práci, ve které kompresor vyniká, a vaše 46 % se zhroutí na 17 %. To je celý příběh za tabulkou výše.
Záporný výsledek pole logů si zaslouží vlastní vysvětlení. Ten vzorek je téměř výhradně tvořen dlouhými řetězci (řádky logů, URL, base64), což znamená málo opakování a vysokou entropii – málo redundance, kterou by kompresor mohl odstranit. Zalomení řádků v pěkné verzi náhodou dávají párovači stabilní hranice. Odstraňte je a LZ77 ztratí tyto kotvy: v tomto vzorku komprese pěkných původních dat při gzip -1 dává 1 508 bajtů, zatímco minifikace a poté komprese dává 1 518 bajtů. O deset bajtů horší.
Seřazeno podle velikosti se páky řadí takto:
| Páka | Ušetřeno proti původnímu pěknému dokumentu (vnořené objednávky) | Cena |
|---|---|---|
| minify | 46,3 % (jen surové bajty) | Čitelnost pryč; vyžaduje build krok |
| gzip -6 | 94,5 % | Trochu CPU serveru; téměř nulová konfigurace |
| minify + gzip -6 | 95,4 % | Obě předchozí, sečtené |
| minify + brotli | 96,9 % | Vyžaduje HTTPS a modul brotli; předkomprimovat statické soubory |
Skok z 0 na 94,5 % je práce gzipu. Skok z 94,5 % na 95,4 % – devět desetin bodu – je práce minify. Tato tabulka rozhoduje o tom, kam patří vaše úsilí.
5. Úroveň komprese je skrytá proměnná
Každý argument, který předpokládá jedinou pevnou úroveň gzipu, stojí na písku, protože úroveň přímo mění marginální hodnotu minify:
| Nastavení přenosu | Doplňkové bajty, které minify ještě ušetří (vnořené objednávky) |
|---|---|
gzip -1 (výchozí gzip_comp_level v Nginx) |
21,0 % |
| gzip -6 (výchozí ve většině standardních knihoven) | 17,0 % |
| gzip -9 | 8,9 % |
| brotli (výchozí kvalita) | 10,6 % |
Čím slabší přenosová komprese, tím více se minify vyplatí. Z toho plyne praktické pravidlo: na běžné instalaci Nginx, kde gzip_comp_level je stále 1, vám minify předá 21 %. Jakmile sladíte na 9 nebo přepnete na brotli, klesne to na 9–11 % a obvykle ho můžete přeskočit.
Než začnete argumentovat kterýmkoli směrem, změřte, na které úrovni skutečně jste:
# Načtěte stejnou odpověď se třemi hodnotami Accept-Encoding a srovnejte, co dorazilo
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
Tři čísla odpovídají na tři otázky najednou: je komprese vůbec zapnutá, na jaké úrovni a kolik místa zbývá pro minify. Pokud se identity a gzip vrátí téměř shodné, komprese vůbec neúčinkuje – a věc k opravě je Nginx, nikoli váš JSON:
gzip on;
gzip_comp_level 6;
gzip_min_length 256; # přeskočit malé odpovědi, jen rostou
gzip_types application/json application/javascript text/css;
6. Kdy se minify skutečně vyplatí
Rozhoduje jeden test: budou ušetřené bajty někde zaznamenány, nebo narazíte na limit, který počítá surové bajty? Pokud ano, projeví se plných 46 %. Pokud je odpověď „existují jen na drátě“, pak 17 % je jejich skutečná hodnota.
Za prvé, těla HTTP požadavků. Většina serverů nedekomprimuje těla požadavků a gzip je konfigurován téměř výhradně pro odpovědi. JSON, který POSTujete, obvykle nemá žádnou ochranu přenosové vrstvy, takže úspora minify přichází v plné ceně. Totéž platí pro webhook callbacky, MQTT telemetrii a WebSocket rámce, které nikdy nevyjednaly permessage-deflate.
Za druhé, kanály s tvrdým bajtovým stropem – a každý z nich počítá dekódované bajty:
| Kanál | Limit | Může gzip pomoci? |
|---|---|---|
| Synchronní payload AWS Lambda | 6 MB | Ne, měřeno po dekódování |
| Jediný dokument Firestore | 1 MiB | Ne |
| Jediná zpráva Kafka (výchozí) | 1 MB | Ne, komprese na straně brokeru je jiná vrstva |
| Kvóta localStorage | ~5 MB (počítáno ve znacích) | Ne |
| URL požadavku GET | Závisí na serveru, obvykle 8 KB | Ne |
V této kategorii není minify optimalizace, je to rozdíl mezi požadavkem, který se odešle, a požadavkem, který selže. Vkládání JSON do query stringu URL je nejostřejší případ: URL se nikdy nekomprimují a mají délkový strop, takže nejprve minifikujte a poté rozhodněte, zda vůbec potřebujete base64.
Za třetí, JSON zapisovaný na disk nebo do logů. Logy a indexy ukládají doslovné bajty bez přenosové vrstvy. Existuje výhrada, která lidi plete: minify pomáhá jen tehdy, když jste od začátku zapisovali pěkný JSON. Ten 200řádkový vzorek logů ušetřil jen 4,1 %, protože jeho řádky už byly kompaktní a odsazení tvořilo téměř nic z celého souboru. Naopak, pokud váš logger volá JSON.stringify(obj, null, 2), vypuštění třetího argumentu jsou peníze zdarma.
Poslední poznámka: když jsou surové bajty skutečně těsné, názvy klíčů jsou obvykle větší páka než odsazení. U media-manifest.json nahrazení jeho 14 jedinečných klíčů k0 až k13 sníží minifikovanou formu z 21 222 B na 18 887 B – dalších 11,0 %. Komprimované se tento zbývající náskok smrskne na 2,4 % pod gzip -6 a 1,3 % pod brotli. Stejný mechanismus jako u všeho výše: odsazení i názvy klíčů jsou opakované strukturální informace a kompresor si stejnou dividendu nárokuje v obou případech.
7. Čtyři případy, kdy to škodí
Neminifikujte soubory, které commitujete. Změňte "qty": 2 na 3 a pěkný soubor ukáže v git diff jeden čistý řádek; minifikovaný soubor přepíše celý řádek a u stovek polí změnu vůbec nenajdete. Je to stejná mechanika, kterou pokrývá Jak řešit konflikty slučování JSON – diff nástroje pracují po řádcích, takže jednořádkový JSON mění každou úpravu na „celý řádek se liší“. Udržujte v repozitáři pěkné soubory a minifikujte jen ve výstupu sestavení nebo při zápisu do úložiště.
Nekomprimujte malé odpovědi. Odpověď obsahující jen {"ok": true} má po přidání odsazení 16 bajtů a po gzip -6 36 bajtů – o dvacet bajtů více, protože slovník a hlavičky gzipu zabírají prostor. Není to teoretické; přesně proto existuje gzip_min_length. Jeho výchozí hodnota 20 bajtů v Nginx je pro produkci hluboko pod realitou, kde je obvyklá spodní hranice 256.
Nečekejte rychlejší parsování. Přiškálování stejné sady objednávek na 4 000 záznamů dává pěkný dokument 2 374 378 bajtů proti 1 282 358 minifikovaným – o 46 % menší – přesto se medián času JSON.parse posune jen z 5,7 ms na 5,0 ms, zhruba o 12 % rychleji. Poloviční velikost není poloviční čas, protože parser vydává většinu rozpočtu na krájení řetězců a převod čísel; přeskočení prázdných znaků je okrajové. Náklady parsování skutečně řídí počet klíčů a obsah řetězců, což podrobně rozebírá Parsování, vykreslování a streamování velkých souborů JSON.
Nedělejte dodatečnou minifikaci u služby, která už běží na brotli nebo gzip -9. Přínos klesne na 9–11 %, zatímco cena – trvalá ztráta čitelnosti při ladění – zůstává. Zkontrolujte místo toho, zda gzip_types vůbec pokrývá application/json. Spousta webů zapne gzip a komprimuje jen text/html, takže JSON zůstane zcela nekomprimovaný; v takové situaci není minify první krok, oprava konfigurace je.
8. Rozhodovací tabulka: který krok první
| Vaše situace | Udělejte nejprve toto | Kde se řadí minify |
|---|---|---|
| Žádná přenosová komprese | Nakonfigurujte gzip_types v Nginx nebo zapněte automatickou kompresi CDN |
Přeskočte; devět desetin přínosu se vypaří |
| Běží gzip -1 (výchozí Nginx) | Zvyšte gzip_comp_level na 6 |
Druhé, má cenu 21 % |
| Už na brotli nebo gzip -9 | Proveďte audit dlouhých klíčů, přidejte stránkování nebo ořežte pole | Přeskočte; zbývá jen 9–11 % |
| Narážíte na 6 MB Lambda nebo 1 MiB Firestore | Minifikujte přímo; počítá se plných 46 % | První |
| Těla POST, webhooky, MQTT | Minifikujte přímo; přenos se tu zřídka uplatní | První |
| Konfigurační soubory do Gitu | Nechte je pěkné | Jen ve výstupu sestavení |
| Logy, telemetrie, offline výpisy | Odstraňte argument null, 2 v loggeru |
To je první krok |
Pravidlo v jedné větě: podívejte se, kterou bajtovou účetní knihu se snažíte zmenšit – tu zaznamenávanou lokálně (disk, kvóta, limit platformy, tělo požadavku), nebo tu, která existuje jen na drátě. Lokální znamená minify; jen na drátě znamená nejprve zapnout brotli nebo gzip.
V praxi spusťte smyčku curl výše, abyste potvrdili, že přenosová komprese běží, poté vzorek prožeňte nástrojem Minify JSON a přečtěte údaj o úspoře. Pokud je toto číslo nad 40 %, zatímco curl hlásí identity a gzip s téměř stejnou velikostí, vaším úzkým hrdlem je konfigurace serveru, nikoli JSON. Pokud jsou dvě čísla curl už blízko, tento dokument toho moc dalšího nabídnout nemůže a měli byste raději ořezávat pole nebo stránkovat – téma Základy návrhu smlouvy JSON API.
Je minifikovaný JSON stále platný JSON?
Ano. minify odstraňuje jen prázdné znaky, které RFC 8259 povoluje kolem strukturálních znaků, a nikdy nezasahuje do obsahu řetězců, takže JSON.parse produkuje hodnotu shodnou s původní. Jedinou obětí je lidská čitelnost.
brotli, nebo gzip?
Na stejném minifikovaném vzorku ušetří brotli 96,9 % a gzip -6 95,4 % – úzký rozdíl. Pro dynamické odpovědi použijte gzip -6, abyste udrželi CPU nízké, a brotli si nechte pro statické soubory doručované přes HTTPS, které můžete předkomprimovat do souborů .br.
Minifikoval jsem JSON, ale API se nezrychlilo. Proč?
Zkontrolujte odpověď pomocí curl -sI, zda má hlavičku Content-Encoding: gzip. Mnoho instalací Nginx má gzip_types ve výchozím nastavení jen text/html, takže JSON nebyl nikdy pokryt – a na drát se dostalo méně než pětina vašich 46 %.
Mají se konfigurační soubory commitovat minifikované?
Ne. Diff nástroje srovnávají řádek po řádku, takže jednořádkový JSON mění každou úpravu na „celý řádek se změnil“ a řešení konfliktů stojí mnohem víc než ušetřené bajty. Udržujte v repozitáři pěkné verze a minifikujte při sestavení.
Jak velká má být odpověď, aby se gzip vyplatil?
Obvyklý práh je 256 bajtů až 1 KB. Odpověď malá jako {"ok": true} – 16 bajtů s odsazením – naroste pod gzip -6 na 36 bajtů, protože slovník a hlavičky stojí víc, než kolik obnoví. Přesně to řídí gzip_min_length.