How to Shrink JSON Payloads and Speed Up Your API
Practical techniques to reduce JSON payload size — minification, gzip and brotli, shorter keys, dropping nulls, pagination — with before and after byte measurements.
Every byte you send over the wire costs time. On a fast desktop connection a bloated JSON response feels instant, but on a congested mobile network the same payload can add hundreds of milliseconds before your app paints a single pixel. Smaller responses mean lower latency, less bandwidth billed by your CDN, and a noticeably snappier experience for users on phones. This guide walks through the techniques that actually move the needle, with real before-and-after byte counts so you can see what each one buys you.
Why payload size matters
Response size affects three things at once. First, latency: larger bodies take more round trips to deliver, especially before TCP's congestion window ramps up. Second, bandwidth: mobile users on metered plans and your own egress bills both scale with bytes shipped. Third, parse time: the browser has to decode and build objects from everything you send, and that work happens on the main thread.
A good habit is to measure first. Drop a representative response into the JSON Size Analyzer to see exactly where the bytes go — which keys repeat, how deep the nesting runs, and what fraction is whitespace.
Step one: minify
The cheapest win is removing insignificant whitespace. Pretty-printed JSON is great for humans and terrible for the network. Consider this response:
{
"user": {
"id": 1024,
"name": "Ada Lovelace",
"email": "ada@example.com",
"roles": [
"admin",
"editor"
]
}
}
That formatted version is about 158 bytes. Minified, it collapses to a single line:
{"user":{"id":1024,"name":"Ada Lovelace","email":"ada@example.com","roles":["admin","editor"]}}
Now it is roughly 95 bytes — a 40 percent reduction with zero loss of information. For a quick pass, run your output through the JSON Minifier. In code, the same thing happens automatically when you skip the indent argument:
// Pretty: do NOT do this in production responses
res.send(JSON.stringify(data, null, 2));
// Minified: the default
res.send(JSON.stringify(data));
Step two: compress with gzip or brotli
Minification removes whitespace; compression removes redundancy. JSON is highly repetitive — the same keys appear in every array element — so it compresses extremely well. Enabling gzip or brotli at the server or CDN layer typically shrinks a JSON body by 70 to 90 percent.
In Express, enable compression with a single middleware:
import compression from "compression";
app.use(compression());
To confirm it is working, request the endpoint and inspect the headers:
curl -s -H "Accept-Encoding: br, gzip" -o /dev/null -w "%{size_download}\n" \
https://api.example.com/users
Compare the byte count against the same request with Accept-Encoding: identity. For a 50 KB array of user objects you might see something like:
identity (raw): 51200 bytes
gzip: 6800 bytes
brotli (quality 5): 5900 bytes
Brotli usually edges out gzip for text, especially at higher quality levels, though it costs more CPU. For static or cacheable responses, pre-compress at maximum quality so you pay that cost once.
Step three: shorten keys
Because JSON repeats every key in every object, long descriptive key names add up fast in large arrays. Compare these two representations of the same list:
[
{"transactionIdentifier":"a1","transactionAmount":42,"transactionCurrency":"USD"},
{"transactionIdentifier":"a2","transactionAmount":17,"transactionCurrency":"EUR"}
]
That is about 150 bytes. Shorten the keys and you get:
[
{"id":"a1","amt":42,"cur":"USD"},
{"id":"a2","amt":17,"cur":"EUR"}
]
Now around 78 bytes — roughly half. The trade-off is readability, so reserve this for high-volume internal APIs or hot paths, and document the mapping. Note that gzip already collapses repeated keys efficiently, so measure before sacrificing clarity; the win is largest on uncompressed or already-compressed binary transports.
Step four: drop nulls and defaults
Sending fields that are null or equal to a known default wastes bytes. If a
client treats a missing field the same as null, omit it entirely.
function stripEmpty(obj) {
const out = {};
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined) continue;
out[key] = value;
}
return out;
}
A record like the following:
{"id":7,"name":"Widget","color":null,"discount":0,"notes":null}
drops to:
{"id":7,"name":"Widget","discount":0}
That is a savings from about 62 bytes to 37 bytes per record. Across a thousand rows it adds up to real money. Just make sure the client and a shared schema agree on the default semantics so an absent field is never ambiguous.
Step five: avoid deep nesting
Deeply nested structures cost bytes in braces and brackets and make parsing slower. A flatter shape is often smaller and easier to consume:
{"address":{"geo":{"location":{"lat":51.5,"lng":-0.12}}}}
versus the flattened equivalent:
{"lat":51.5,"lng":-0.12}
The JSON Size Analyzer reports nesting depth, so you can spot structures that are deeper than they need to be.
Step six: prefer arrays over repeated keys
When every object in a list shares the same keys, a columnar or tabular layout removes the per-row key overhead entirely. Instead of:
[
{"id":1,"name":"A","active":true},
{"id":2,"name":"B","active":false}
]
send a header plus rows:
{"cols":["id","name","active"],"rows":[[1,"A",true],[2,"B",false]]}
For large tables this can cut size by 30 to 50 percent before compression,
because the keys appear once rather than once per row. The client reassembles
objects by zipping cols with each row.
Step seven: paginate
The largest payload is the one you never send. If a client only renders 25 rows, do not return 10,000. Use cursor-based pagination and return a token for the next page:
{"items":[],"nextCursor":"eyJpZCI6MjV9","hasMore":true}
Pagination keeps individual responses small and predictable, which also helps your caches and your database.
Measure before and after
Optimization without measurement is guesswork. Capture the byte size before and after each change. A quick script makes this repeatable:
before=$(curl -s https://api.example.com/v1/users | wc -c)
after=$(curl -s https://api.example.com/v2/users | wc -c)
echo "before=$before after=$after saved=$((before - after))"
For ad-hoc analysis, paste both versions into the JSON Size Analyzer and compare. When you are happy with the shape, run the final output through the JSON Minifier to confirm there is no leftover whitespace.
Putting it all together
Stack the techniques in order of effort versus payoff: enable compression first because it is one line and wins the most, then minify, then trim nulls and defaults, then reconsider your structure with flattening, columnar arrays, and pagination. Always measure each step so you keep the wins that matter and skip the ones that only hurt readability.
Ready to trim your responses? Start by analyzing a real payload with the JSON Size Analyzer and minifying it with the JSON Minifier — both run entirely in your browser, so your data never leaves your machine. Explore the rest of the toolkit at /tools/ and read more on the blog.
Keep reading
Validating JSON with JSON Schema
Learn how JSON Schema works, how to generate a schema from sample data, and how to validate documents with clear, path-based error messages.
JSON vs JSON5 vs JSONC — What's the Difference?
Comments, trailing commas, and unquoted keys — understand JSON5 and JSONC, where each is used, and how to convert them to strict JSON.
How to Format JSON (and Why It Matters)
A practical guide to beautifying, indenting, and minifying JSON — and when to use each, with tips for debugging messy API responses.