JSON Minify vs Gzip: What Actually Shrinks a Payload
Minifying JSON before sending it over a compressed connection is one of those optimisations that feels productive and usually is not. The only way to settle it is to measure, so this guide measures — three documents, four sizes each, produced by a script you can run yourself.
In short
Over a gzipped or brotlied connection, minifying JSON typically saves between 3% and 17% of the transferred bytes, because compression already collapses the repeated whitespace that minification removes. Minification matters far more where compression does not apply: hard uncompressed size limits such as AWS Lambda's 6 MB payload or Firestore's 1 MiB document, log lines, and values embedded in URLs.
Two different operations that get conflated
Minification is a transformation of the document. It parses the JSON and re-serialises it without the insignificant whitespace, producing a different sequence of bytes that parses to exactly the same value. It is lossless with respect to the data and permanent with respect to the file: what you save, you save on disk, in memory, in the log line, and in the size check.
Compression is a transformation of the transfer. Gzip and brotli encode the bytes into a smaller form that has to be decoded before anything can read it. The saving exists on the wire and in the cache; the moment the client decompresses, the original bytes are back. If your constraint is a limit measured after decoding — and most platform limits are — compression buys you nothing at all.
The two overlap because both target redundancy, and whitespace is redundant. That overlap is exactly why stacking them yields less than you would expect: by the time gzip runs, the indentation minification would have removed is the easiest thing in the document for it to encode. Understanding where the two do not overlap is what makes the decision obvious in each case.
What minification actually removes
Surprisingly little, and only one category of thing. RFC 8259 permits whitespace — space, horizontal tab, line feed and carriage return — before and after any structural character: the braces, brackets, colons and commas. Nothing else in a JSON document is optional. Key names are data, so they stay. Numbers keep their digits. String contents are untouched. A minifier is therefore JSON.parse followed by JSON.stringify with no indent argument.
This is a real difference from minifying JavaScript or CSS, where a minifier renames variables, drops dead code and rewrites values. There is no equivalent for JSON, which is why any tool promising dramatic JSON minification is either counting whitespace it inflated in the first place or is quietly changing your data. Watch for the changes that are not lossless: dropping keys whose value is null, rounding floats, or reordering keys are transformations of the document, not of its formatting.
The corollary is that minification's saving is proportional to how much indentation the document had. A two-space-indented, deeply nested document is mostly whitespace by byte count; a flat array of long strings is barely affected. That is the variable the measurements below are designed to expose. If you want the numbers for your own payload rather than these samples, the JSON Size Calculator reports raw, minified and gzipped bytes side by side, and JSON Minify does the transformation itself.
// Minification in one line: parse, then re-serialise with no indent
const pretty = '{\n "id": 1,\n "tags": [\n "a",\n "b"\n ]\n}';
JSON.stringify(JSON.parse(pretty)); // {"id":1,"tags":["a","b"]}
// Whitespace INSIDE a string is data and survives untouched
JSON.stringify(JSON.parse('{"note": "two spaces"}')); // {"note":"two spaces"}
// Not minification - these change the document, not its formatting
const lossy = Object.fromEntries(
Object.entries(obj).filter(([, v]) => v !== null), // silently drops null keys
);What gzip and brotli actually do
Gzip, specified in RFC 1952 and wrapping the DEFLATE stream of RFC 1951, works in two stages. First LZ77 replaces repeated byte sequences with a back-reference — a distance and a length pointing at an earlier occurrence within a 32 KB sliding window. Then Huffman coding assigns short bit patterns to frequent symbols and long ones to rare symbols. JSON is close to a best case for both: the same key names recur in every element of an array, the punctuation set is tiny, and indentation runs are long and identical.
Brotli, specified in RFC 7932, uses the same two ideas with a larger window and one significant addition: a built-in static dictionary of roughly thirteen thousand common strings drawn from web text, plus transformations that can be applied to them. For small payloads that dictionary is a genuine head start, because a short document has no history for LZ77 to reference yet. Brotli also offers finer quality levels; the highest ones are slow enough that they are meant for pre-compressed static assets, not for a response generated per request.
The practical consequence for JSON is that both algorithms are extremely good at the exact redundancy minification targets. Two hundred lines of two-space indentation is a handful of back-references. The keys that repeat across a thousand array elements collapse to almost nothing. Whatever survives compression is the actual information content of your payload, and no amount of formatting discipline reduces that.
Measured: three documents, four sizes each
The script below builds three documents chosen to bracket the realistic range: a small service config of about seven keys; a fifty-item array of order records with repeating key names and varied values; and a tree six levels deep with long descriptive key names and pseudo-random leaf values, so the nesting is not artificially repetitive. For each it reports the pretty-printed size, the minified size, and the gzip and brotli sizes of the minified bytes.
The pattern is consistent. Minification is a large win against the raw file — 26%, 23% and 60% here — and a small one against the compressed transfer. Gzipping the pretty-printed bytes directly instead gives 196, 851 and 1,882 bytes respectively, so minifying first saves 17 bytes, 30 bytes and 315 bytes: 8.7%, 3.5% and 16.7% of the transfer. The deeply nested document benefits most, which is exactly what the previous section predicts — it had the most indentation to begin with.
Brotli beats gzip on all three, by 14% to 33%. That is the more interesting lever: switching a JSON API from gzip to brotli usually saves more bytes than minifying ever will, and it is a server configuration change rather than a code change. Note also that brotli's advantage on the small config, 137 bytes against 179, is proportionally the largest — that is its static dictionary doing the work on a document too short to have useful LZ77 history of its own.
| Document | Raw (2-space) | Minified | Gzip | Brotli |
|---|---|---|---|---|
| Small config, ~7 keys | 284 | 211 | 179 | 137 |
| 50-item order array | 10,427 | 8,026 | 821 | 554 |
| Deeply nested, 6 levels | 26,386 | 10,548 | 1,567 | 1,341 |
import zlib from "node:zlib";
const config = {
name: "checkout-service", version: "2.4.1", port: 8080, logLevel: "info",
database: { host: "db.internal", port: 5432, ssl: true, poolSize: 10 },
features: { retries: true, tracing: false, maxRetryAttempts: 3 },
};
const orders = Array.from({ length: 50 }, (_, i) => ({
orderId: "ORD-2026-" + String(i + 1).padStart(5, "0"),
customerId: "CUST-" + String((i * 37) % 999).padStart(4, "0"),
status: ["pending", "shipped", "delivered", "cancelled"][i % 4],
totalAmount: Number((19.99 + i * 3.5).toFixed(2)),
currency: "USD",
itemCount: (i % 5) + 1,
}));
const b = (s) => Buffer.byteLength(s, "utf8");
function sizes(label, obj) {
const pretty = JSON.stringify(obj, null, 2);
const min = JSON.stringify(obj);
return {
label,
raw: b(pretty),
minified: b(min),
gzip: zlib.gzipSync(Buffer.from(min), { level: 9 }).length,
brotli: zlib.brotliCompressSync(Buffer.from(min)).length,
gzipOfPretty: zlib.gzipSync(Buffer.from(pretty), { level: 9 }).length,
};
}
console.table([sizes("Small config", config), sizes("50-item array", orders)]);
// Small config { raw: 284, minified: 211, gzip: 179, brotli: 137, gzipOfPretty: 196 }
// 50-item array { raw: 10427, minified: 8026, gzip: 821, brotli: 554, gzipOfPretty: 851 }
// Deeply nested { raw: 26386, minified: 10548, gzip: 1567, brotli: 1341, gzipOfPretty: 1882 }Why long key names barely matter after gzip
The other perennial suggestion is shortening key names — customerEmailAddress becomes e, and the payload gets dramatically smaller. It does, before compression. The same measurement run compared a fifty-element array with three long descriptive keys against the identical data with single-letter keys: 5,491 bytes versus 2,391, a 56% reduction. After gzip at level 9 the same two documents were 379 and 294 bytes — a 22% reduction, and 85 bytes in absolute terms.
The reason is structural. A key name that appears fifty times is stored once by LZ77 and referenced forty-nine times, and a back-reference costs roughly the same whether it points at a two-character key or a twenty-character one. You pay for a long key once per document, not once per element. That is why the intuition built on raw byte counts is so badly calibrated: the cost you are optimising away was already amortised.
Set against 85 bytes, single-letter keys cost you readable logs, self-documenting API responses, greppable payloads and a mapping layer every consumer has to maintain. There are contexts where the trade is worth it — a telemetry beacon sent from every page view, a websocket frame at high frequency — but those are the cases where the better question is whether JSON is the right format at all, rather than how hard you can squeeze it. For everything else, keep the descriptive names and turn on brotli.
When minification genuinely earns its place
Hard size limits are the strongest case, because they are almost always measured on the uncompressed bytes. AWS Lambda caps a synchronous invocation's request and response payload at 6 MB; Firestore caps a document at 1 MiB, which is 1,048,576 bytes; DynamoDB caps an item at 400 KB; many message queues and webhook receivers publish similar ceilings. Compression is irrelevant to all of them — the platform decodes first and then measures. Here minification's 20 to 30 per cent is the difference between shipping and a rejected request.
URLs are the second case. A JSON value in a query string or a fragment is not compressed at all, and browsers and proxies impose practical length limits well below the theoretical ones. Minifying before encoding is free bytes, and it composes with the encoding step rather than competing with it. Logs are the third: structured log lines are usually written one JSON object per line, and pretty-printing them multiplies both the storage bill and the ingestion cost of a platform that charges per gigabyte.
Finally, storage at rest. A million pretty-printed documents in an object store or a JSONB column carry their indentation forever, and the query engine reads it on every scan. Minify on write and pretty-print on read — that is the right split, and it is why every serious tool separates the two. Use JSON Formatter when a human needs to read the document and JSON to One Line when a machine does.
Check what your server is actually sending
Before optimising anything, confirm compression is on. The response carries Content-Encoding: gzip or br when the body was compressed, and the client signals what it accepts with Accept-Encoding. The failure mode is quiet: a proxy strips the header, a CDN rule excludes application/json, or the origin only compresses text/html, and every response goes out uncompressed while everyone assumes otherwise. A single curl tells you.
One subtlety in the commands below: if you pass --compressed, curl adds the Accept-Encoding header and transparently decodes the body, so the size it reports is the decoded size. If you set the header manually instead, curl leaves the body encoded and the size it reports is the real wire size. Running both is the quickest honest before-and-after. Note too that -I sends a HEAD request, which some APIs do not implement — dump the headers of a GET instead if HEAD misbehaves.
Two more things worth checking while you are there. Content-Encoding is end-to-end and applies to the body only, so it should not be confused with Transfer-Encoding, which is hop-by-hop. And a response that is already in a compressed format — an image, a zip archive — should not be compressed again; recompressing costs CPU and often adds bytes. For JSON, always compress; the only question is whether brotli is available, and it has been supported by every current browser over HTTPS for years.
# Is the response compressed at all, and with what?
curl -sI -H 'Accept-Encoding: gzip, br' https://api.example.com/orders \
| grep -i -E 'content-encoding|content-length|vary'
# Real wire bytes: set the header manually so curl does NOT decode
curl -s -o /dev/null -H 'Accept-Encoding: gzip, br' \
-w 'wire: %{size_download} bytes\n' https://api.example.com/orders
# Decoded bytes for comparison (--compressed makes curl decode)
curl -s -o /dev/null --compressed \
-w 'decoded: %{size_download} bytes\n' https://api.example.com/orders
# If HEAD is not implemented, read the headers of a GET instead
curl -s -D - -o /dev/null -H 'Accept-Encoding: br' https://api.example.com/ordersSources
Frequently asked questions
Should I minify JSON if my server already gzips responses?
Usually not for the bytes alone. In the measurements above, minifying before gzip saved between 3.5% and 16.7% of the transferred size — real, but small next to the effort of maintaining a minification step. Minify anyway if the document is written to a log, stored at rest, embedded in a URL, or checked against an uncompressed size limit, because in all of those cases compression does not apply.
Is brotli always better than gzip for JSON?
On size, yes for typical JSON — the three documents measured here came out 14% to 33% smaller with brotli at default settings. The trade-off is CPU at high quality levels, which is why servers usually pre-compress static assets aggressively and compress dynamic responses at a lower level. Keep gzip configured as the fallback: the client advertises what it supports in Accept-Encoding and the server picks.
Does minified JSON parse faster?
Marginally, and not for the reason people expect. The parser still has to scan every byte, so fewer bytes means slightly less scanning, but skipping whitespace is close to the cheapest operation a JSON parser performs. The measurable win is in transfer and memory, not in parse time. If parsing is genuinely your bottleneck, the answer is streaming or a different format, not formatting.
Will minifying ever change my data?
Not if the tool is doing minification. Removing insignificant whitespace is defined by RFC 8259 to preserve the value exactly, and round-tripping through JSON.parse and JSON.stringify proves it. What can change data is a tool that also drops null values, reorders keys, strips duplicates or re-formats numbers — those are separate transformations. Numbers are the one place to watch: a value beyond IEEE 754 double precision, such as a 64-bit ID, can lose digits in any parse-and-re-serialise round trip, minification included.
How do I find out what my payload costs right now?
Paste it into the JSON Size Calculator, which reports raw, minified, pretty-printed and gzipped bytes together with key counts and nesting depth, so you can see which keys dominate. For the transfer side, curl the endpoint with an explicit Accept-Encoding header and read the downloaded size — that is the number your users actually pay for.
Paste a document into the JSON Size Calculator for raw, minified and gzipped bytes — plus the keys eating the budget.
Measure your payloadKeep working
Related tools
JSON Size Calculator
Measure exactly what a JSON payload costs — raw, minified, pretty-printed and gzipped bytes — and see which keys are eating the budget.
JSON Minifier
Compress JSON to its smallest valid form by stripping every unnecessary character — with full validation built in.
JSON Formatter
Format, beautify, and validate JSON instantly in a professional VS Code-style editor — free, fast, and 100% private.