Hören Sie auf, JSON-Komprimierung in der falschen Reihenfolge zu machen: minify vs. gzip vs. brotli, gemessen
Fazit: Minifizieren spart 30–50 % des rohen JSON, aber gzip löscht den größten Teil dieses Gewinns wieder aus. Gemessene Zahlen über reale Formen und die vier Fälle, in denen Minifizieren es schlimmer macht.

Kurz gesagt: Trennen Sie zuerst Transportkomprimierung von strukturellem Beschneiden. Ist gzip oder brotli noch nicht aktiv, reparieren Sie die Serverkonfiguration – allein gzip -6 erreicht 94,5 % und lässt minify nur noch 0,9 Punkte wert. Die vollen 46,3 % von minify landen nur, wenn die Byte-Rechnung lokal geführt wird: Request-Bodies, Lambdas 6 MB, Firestores 1 MiB oder JSON, das auf Platte und in Logs geschrieben wird.
Das „35 % gespart“-Abzeichen Ihres Lieblings-Minifiers ist ehrlich – es misst rohe Bytes. Ihre API läuft auf gzip oder brotli, und diese beiden verzehren genau dieselbe Redundanz. Beide Zahlen zu multiplizieren ergibt die falsche Antwort.
1. „Komprimierung“ in zwei verschiedene Dinge zerlegen
Was Leute JSON-Komprimierung nennen, sind eigentlich zwei unzusammenhängende Operationen, und sie zu verwechseln ist der Anfang jeder falschen Schlussfolgerung.
| minify (Whitespace entfernen) | gzip / brotli (Transportkomprimierung) | |
|---|---|---|
| Natur | Eine Dokumenttransformation | Eine Transporttransformation |
| Ausgabe | Weiterhin gültiger, lesbarer JSON-Text | Binär; bis zum Dekodieren unbrauchbar |
| Wo der Gewinn landet | Platte, Speicher, Logs, Größenlimits | Nur auf der Leitung und in Caches |
| Nach dem Dekodieren | Bytes bleiben kleiner | Bytes kehren zur Originalgröße zurück |
| Wer es besitzt | Ihr Build-Skript, Ihre Serializer-Argumente | Nginx, CDN, Server-Framework |
Die letzten beiden Zeilen sind die entscheidenden. Plattformlimits werden nach dem Dekodieren gemessen: AWS Lambdas 6-MB-Payload-Grenze, Firestores 1 MiB pro Dokument. gzip einzuschalten bringt Ihnen dort keinen einzigen Byte, weil beide das dekodierte Ergebnis zählen.
2. minify entfernt genau eine Sache: strukturellen Whitespace
RFC 8259 erlaubt Whitespace (space, \t, \n, \r) um jedes Strukturzeichen. Nichts anderes in einem JSON-Dokument ist optional: Schlüsselnamen sind Daten, Zahlen behalten jede Ziffer, und String-Inhalte sind Byte für Byte unantastbar.
Das macht die ganze Operation zu einer Zeile:
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
Erwähnenswert sind drei Operationen, die unter „Komprimierung“ abgelegt werden, aber tatsächlich das Dokument verändern: Schlüssel mit dem Wert null entfernen, Floats runden und Schlüssel umordnen. Sie ändern das Dokument, statt es neu zu formatieren, jedes Werkzeug, das „tiefe Komprimierung“ bewirbt, braucht also den Segen Ihres Konsumenten, bevor Sie es aktivieren.
Weil minify nur Whitespace entfernt, hängt sein Nutzen fast vollständig davon ab, wie viel Ihrer Datei Einrückung ist. Das wird sich in den Tabellen unten immer wieder zeigen.
3. Die Messung: vier Dokumente, drei gzip-Stufen, plus brotli
Umgebung: Node v22.13.1, zlib.gzipSync auf Stufe 1 / 6 / 9, brotliCompressSync mit Standardqualität (11). Die vier Beispiele decken bewusst die Formen ab, die Sie tatsächlich ausliefern: 50 verschachtelte Bestellungen (tiefe Struktur, wiederholte Schlüssel), eine Konfiguration mit 6 Ebenen (lange Schlüssel, kurze Werte), ein Log-Array mit 200 Zeilen (lange Strings, fast keine Einrückung) und die echte media-manifest.json dieser Website (gemischte Form, URL-lastige Strings).
Schritt eins: rohe Bytes.
| Beispiel | hübsch (2 Leerzeichen) | hübsch (4 Leerzeichen) | nach minify | minify spart |
|---|---|---|---|---|
| 50 verschachtelte Bestellungen | 29.551 B | 40.761 B | 15.881 B | 46,3 % |
| Konfiguration mit 6 Ebenen | 25.450 B | 38.012 B | 11.679 B | 54,1 % |
| Log-Array mit 200 Zeilen | 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 % |
Hierher kommt das „35 % gespart“-Abzeichen, und es lügt nicht. Das Problem erscheint in Schritt zwei.
Schritt zwei: jede Variante komprimieren und dann fragen, wie viel minify noch bringt.
| Beispiel | zusätzlich zu gzip -1 | zusätzlich zu gzip -6 | zusätzlich zu gzip -9 | zusätzlich zu brotli |
|---|---|---|---|---|
| 50 verschachtelte Bestellungen | 21,0 % | 17,0 % | 8,9 % | 10,6 % |
| Konfiguration mit 6 Ebenen | 74,2 % | 59,1 % | 58,0 % | 56,4 % |
| Log-Array mit 200 Zeilen | -0,7 % | -0,4 % | 0,6 % | -0,3 % |
| media-manifest.json | 1,5 % | 1,0 % | 1,0 % | 1,6 % |
Dasselbe Dokument mit verschachtelten Bestellungen, das 46,3 % seiner rohen Bytes verliert, behält davon nur 17,0 %, sobald gzip -6 im Spiel ist. Das Log-Array geht glatt ins Negative: minify-dann-komprimieren ist etwas größer als das hübsche Original zu komprimieren. Negativ ist kein Bug, es ist genau das, was Sie erwarten sollten, und der nächste Abschnitt erklärt warum.
Schritt drei: Gesamtreduktion gegenüber dem ursprünglichen hübschen Dokument – die Zahl, die tatsächlich über die Leitung reist.
| Beispiel | nur gzip -6 | minify + gzip -6 | minify + brotli |
|---|---|---|---|
| 50 verschachtelte Bestellungen | 94,5 % | 95,4 % | 96,9 % |
| Konfiguration mit 6 Ebenen | 96,0 % | 98,4 % | 99,0 % |
| Log-Array mit 200 Zeilen | 89,9 % | 89,9 % | 95,0 % |
| media-manifest.json | 83,5 % | 83,6 % | 86,1 % |
4. Warum gzip den Verdienst von minify frisst
gzip ist LZ77 gefolgt von Huffman-Kodierung. Der zentrale LZ77-Schritt ist „im 32-KB-Fenster zurückblicken, eine wiederholte Sequenz finden, sie durch einen (Distanz, Länge)-Zeiger ersetzen“. Einrückung ist das leichteste mögliche Ziel dafür: Jede Verschachtelungsebene erzeugt eine identische Folge von Leerzeichen, und Dutzende benachbarter Zeilen wiederholen dieselbe Kombination. Huffman weist dann häufigen Symbolen kurze Codes zu, sodass ein Muster wie \n plus acht Leerzeichen – eines, das tausendmal in einer Datei vorkommen kann – am Ende zwei oder drei Bits kostet.
Die Bytes, die minify löscht, sind also genau die Bytes, die der Kompressor fast umsonst kollabiert hätte. Es selbst zu tun heißt, Arbeit zu wiederholen, in der der Kompressor bereits glänzt, und Ihre 46 % schrumpfen auf 17 %. Das ist die ganze Geschichte hinter der obigen Tabelle.
Das negative Log-Array-Ergebnis verdient eine eigene Erklärung. Dieses Beispiel besteht fast vollständig aus langen Strings (Logzeilen, URLs, Base64), was wenige Wiederholungen und hohe Entropie bedeutet – wenig Redundanz, die der Kompressor überhaupt entfernen könnte. Die Zeilenumbrüche in der hübschen Version geben dem Matcher zufällig stabile Grenzen. Entfernt man sie, verliert LZ77 diese Anker: In diesem Beispiel ergibt das Komprimieren des hübschen Originals mit gzip -1 1.508 Bytes, während minify-dann-komprimieren 1.518 Bytes ergibt. Zehn Bytes schlechter.
Nach Größe geordnet stellen sich die Hebel so auf:
| Hebel | Gespart gegenüber dem ursprünglichen hübschen Dokument (verschachtelte Bestellungen) | Kosten |
|---|---|---|
| minify | 46,3 % (nur rohe Bytes) | Lesbarkeit weg; braucht einen Build-Schritt |
| gzip -6 | 94,5 % | Ein wenig Server-CPU; nahezu keine Konfiguration |
| minify + gzip -6 | 95,4 % | Die beiden oben, addiert |
| minify + brotli | 96,9 % | Braucht HTTPS und das brotli-Modul; statische Assets vorab komprimieren |
Der Sprung von 0 auf 94,5 % ist gzips Arbeit. Der Sprung von 94,5 % auf 95,4 % – neun Zehntel eines Punktes – ist minifys. Diese Tabelle entscheidet, wohin Ihr Aufwand gehört.
5. Die Kompressionsstufe ist die versteckte Variable
Jede Argumentation, die eine einzige feste gzip-Stufe annimmt, steht auf Sand, denn die Stufe ändert direkt den Grenzwert von minify:
| Transporteinstellung | Zusätzliche Bytes, die minify noch spart (verschachtelte Bestellungen) |
|---|---|
gzip -1 (Nginx gzip_comp_level-Standard) |
21,0 % |
| gzip -6 (Standard in den meisten Standardbibliotheken) | 17,0 % |
| gzip -9 | 8,9 % |
| brotli (Standardqualität) | 10,6 % |
Je schwächer Ihre Transportkomprimierung, desto mehr zahlt sich minify aus. Das ergibt eine praktische Regel: Auf einer Standard-Nginx-Installation, bei der gzip_comp_level noch 1 ist, bringt minify 21 %. Haben Sie auf 9 getunt oder auf brotli gewechselt, sind es nur noch 9–11 %, und Sie können es meist überspringen.
Bevor Sie in die eine oder andere Richtung argumentieren, messen Sie, auf welcher Stufe Sie tatsächlich stehen:
# Fetch the same response with three Accept-Encoding values and compare what landed
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
Drei Zahlen beantworten drei Fragen auf einmal: Ist Komprimierung überhaupt an, auf welcher Stufe, und wie viel Raum bleibt für minify. Kommen identity und gzip nahezu identisch zurück, ist Komprimierung gar nicht aktiv – und zu reparieren ist Nginx, nicht Ihr 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. Wann minify wirklich etwas bringt
Ein Test entscheidet: Werden die gesparten Bytes irgendwo festgehalten, oder stoßen Sie an ein Limit, das rohe Bytes zählt? Ja → die vollen 46 % landen. Lautet die Antwort „es existiert nur auf der Leitung“, dann sind 17 % sein echter Wert.
Erstens, HTTP-Request-Bodies. Die meisten Server dekomprimieren Request-Bodies nicht, und gzip ist fast ausschließlich für Antworten konfiguriert. Das JSON, das Sie POSTen, hat meist gar keinen Transportschutz, minifys Ersparnis kommt also zum vollen Preis an. Dasselbe gilt für Webhook-Callbacks, MQTT-Telemetrie und WebSocket-Frames, die nie permessage-deflate verhandelt haben.
Zweitens, Kanäle mit harter Byte-Obergrenze – und jeder davon zählt dekodierte Bytes:
| Kanal | Limit | Kann gzip helfen? |
|---|---|---|
| AWS Lambda synchroner Payload | 6 MB | Nein, nach dem Dekodieren gemessen |
| Firestore einzelnes Dokument | 1 MiB | Nein |
| Kafka einzelne Nachricht (Standard) | 1 MB | Nein, Broker-seitige Komprimierung ist eine andere Schicht |
| localStorage-Quota | ~5 MB (in Zeichen gezählt) | Nein |
| GET-Anfrage-URL | Serverabhängig, 8 KB ist üblich | Nein |
In dieser Kategorie ist minify keine Optimierung, es ist der Unterschied zwischen einer Anfrage, die abgeschickt wird, und einer, die scheitert. JSON in einen URL-Query-String einzubetten ist der schärfste Fall: URLs werden nie komprimiert und haben eine Längenobergrenze, also zuerst minifizieren und dann entscheiden, ob Sie überhaupt Base64 brauchen.
Drittens, JSON, das auf Platte oder in Logs geschrieben wird. Logs und Indizes speichern wörtliche Bytes ohne Transportebene. Es gibt einen Vorbehalt, der Leute stolpern lässt: minify hilft nur, wenn Sie überhaupt hübsches JSON geschrieben haben. Dieses Log-Beispiel mit 200 Zeilen sparte nur 4,1 %, weil seine Zeilen bereits kompakt waren und Einrückung fast nichts der Datei ausmachte. Umgekehrt, wenn Ihr Logger JSON.stringify(obj, null, 2) aufruft, ist das Weglassen des dritten Arguments gratis Geld.
Eine letzte Anmerkung: Sind rohe Bytes wirklich knapp, sind Schlüsselnamen meist ein größerer Hebel als Einrückung. Bei media-manifest.json bringt das Ersetzen der 14 eindeutigen Schlüssel durch k0 bis k13 die minifizierte Form von 21.222 B auf 18.887 B – weitere 11,0 %. Komprimiert schrumpft dieser verbleibende Vorsprung auf 2,4 % unter gzip -6 und 1,3 % unter brotli. Derselbe Mechanismus wie oben: Einrückung und Schlüsselnamen sind beide wiederholte strukturelle Information, und der Kompressor holt sich so oder so dieselbe Dividende.
7. Vier Fälle, in denen es nach hinten losgeht
Committete Dateien nicht minifizieren. Ändern Sie "qty": 2 zu 3, und die hübsche Datei zeigt eine saubere Zeile in git diff; die minifizierte Datei schreibt die ganze Zeile neu, und bei Hunderten von Feldern finden Sie die Änderung überhaupt nicht. Das ist dieselbe Mechanik wie in JSON-Merge-Konflikte auflösen – Diff-Werkzeuge arbeiten zeilenweise, einzeiliges JSON macht also jede Bearbeitung zu „die ganze Zeile unterscheidet sich“. Behalten Sie hübsche Dateien im Repository und minifizieren Sie nur in Build-Ausgaben oder beim Schreiben in den Speicher.
Kleine Antworten nicht komprimieren. Eine Antwort, die nur {"ok": true} enthält, ist mit Einrückung 16 Bytes und nach gzip -6 36 Bytes – zwanzig Bytes größer, weil gzips eigene Wörterbuch- und Kopfdaten Platz belegen. Das ist nicht theoretisch; genau deshalb existiert gzip_min_length. Sein Nginx-Standard von 20 Bytes ist für die Produktion viel zu niedrig, 256 ist die übliche Untergrenze.
Kein schnelleres Parsen erwarten. Skaliert man dieselbe Bestellmenge auf 4.000 Datensätze, ist das hübsche Dokument 2.374.378 Bytes gegenüber 1.282.358 minifiziert – 46 % kleiner – doch die mediane JSON.parse-Zeit bewegt sich nur von 5,7 ms auf 5,0 ms, etwa 12 % schneller. Die Größe zu halbieren halbiert nicht die Zeit, weil der Parser den größten Teil seines Budgets für das Schneiden von Strings und das Konvertieren von Zahlen ausgibt; Whitespace zu überspringen ist nebensächlich. Was die Parse-Kosten tatsächlich treibt, sind Schlüsselzahl und String-Inhalt, was Große JSON-Dateien parsen, rendern und streamen im Detail aufschlüsselt.
Nicht nachträglich auf einen Dienst aufsetzen, der bereits brotli oder gzip -9 fährt. Der Nutzen fällt auf 9–11 %, während die Kosten – dauerhafter Verlust der Lesbarkeit beim Debuggen – bleiben. Prüfen Sie stattdessen, ob gzip_types überhaupt application/json abdeckt. Viele Seiten aktivieren gzip und komprimieren nur text/html, sodass JSON völlig unkomprimiert bleibt; in dieser Situation ist minify nicht Schritt eins, Ihre Konfiguration zu reparieren ist es.
8. Entscheidungstabelle: welcher Schritt zuerst
| Ihre Situation | Zuerst tun | Wo minify rangiert |
|---|---|---|
| Gar keine Transportkomprimierung | Nginx gzip_types konfigurieren oder CDN-Auto-Komprimierung aktivieren |
Überspringen; neun Zehntel des Gewinns verdampfen |
| gzip -1 läuft (Nginx-Standard) | gzip_comp_level auf 6 erhöhen |
Zweitens, 21 % wert |
| Bereits auf brotli oder gzip -9 | Lange Schlüssel prüfen, Paginierung ergänzen oder Felder beschneiden | Überspringen; nur 9–11 % übrig |
| Lambda 6 MB oder Firestore 1 MiB erreicht | Direkt minifizieren; die vollen 46 % zählen | Erstens |
| POST-Bodies, Webhooks, MQTT | Direkt minifizieren; Transport greift hier selten | Erstens |
| Konfigurationsdateien, die nach Git gehen | Hübsch halten | Nur in Build-Ausgaben |
| Logs, Telemetrie, Offline-Dumps | Das null, 2-Argument im Logger entfernen |
Das ist Schritt eins |
Die Ein-Satz-Regel: Sehen Sie darauf, welche Byte-Rechnung Sie verkleinern wollen – eine lokal geführte (Platte, Quota, Plattformlimit, Request-Body) oder eine, die nur auf der Leitung existiert. Lokal zuerst heißt minify; nur-Leitung heißt zuerst brotli oder gzip einschalten.
In der Praxis führen Sie die curl-Schleife oben aus, um zu bestätigen, dass Transportkomprimierung live ist, und schicken dann ein Beispiel durch das JSON-Minify-Werkzeug und lesen dessen Ersparniszahl. Liegt diese Zahl über 40 %, während curl identity und gzip bei nahezu gleicher Größe meldet, ist Ihr Engpass die Serverkonfiguration, nicht JSON. Sind die beiden curl-Zahlen bereits nahe beieinander, hat dieses Dokument wenig zu geben, und Sie sollten stattdessen Felder beschneiden oder paginieren – das Thema von Kernpunkte für den Entwurf von JSON-API-Verträgen.
Ist minifiziertes JSON noch gültiges JSON?
Ja. minify entfernt nur den Whitespace, den RFC 8259 um Strukturzeichen erlaubt, und berührt String-Inhalte nie, JSON.parse erzeugt also einen identischen Wert. Das einzige Opfer ist die menschliche Lesbarkeit.
brotli oder gzip?
Beim selben minifizierten Beispiel spart brotli 96,9 % und gzip -6 95,4 % – eine schmale Lücke. Verwenden Sie gzip -6 für dynamische Antworten, um die CPU niedrig zu halten, und reservieren Sie brotli für per HTTPS ausgelieferte statische Assets, die Sie vorab in .br-Dateien komprimieren können.
Ich habe mein JSON minifiziert, aber die API wurde nicht schneller. Warum?
Prüfen Sie die Antwort mit curl -sI auf einen Content-Encoding: gzip-Header. Viele Nginx-Setups setzen gzip_types standardmäßig nur auf text/html, sodass JSON nie abgedeckt war – und weniger als ein Fünftel Ihrer 46 % erreichte je die Leitung.
Sollten Konfigurationsdateien minifiziert committet werden?
Nein. Diff-Werkzeuge vergleichen zeilenweise, einzeiliges JSON macht also jede Bearbeitung zu „die ganze Zeile hat sich geändert“, und Konflikte aufzulösen kostet weit mehr als die gesparten Bytes. Behalten Sie hübsche Versionen im Repository und minifizieren Sie zur Build-Zeit.
Wie groß sollte eine Antwort sein, bevor gzip sich lohnt?
Der übliche Schwellenwert ist 256 Bytes bis 1 KB. Eine Antwort so klein wie {"ok": true} – mit Einrückung 16 Bytes – wächst unter gzip -6 auf 36 Bytes, weil Wörterbuch und Header mehr kosten, als sie zurückgewinnen. Genau das steuert gzip_min_length.