← Back to Blog
·8 min read

JSON Minification vs Compression: You're Optimizing the Wrong Thing

I have seen teams spend engineering time carefully minifying JSON API responses — stripping whitespace, shortening field names — while running their web server without HTTP compression enabled. They were optimizing the wrong thing by orders of magnitude. Here is the data that should settle this debate.

The numbers: what compression actually does to JSON

Consider a typical API response — a list of user objects with standard fields. Pretty-printed JSON has whitespace and newlines. Minified JSON removes them. Compressed JSON applies gzip or brotli on top of either. Here is what happens to a representative 100KB JSON payload:

Pretty-printed JSON:     100,000 bytes (100 KB)
Minified JSON:            82,000 bytes (18% reduction)
Pretty + gzip:            14,200 bytes (86% reduction)
Minified + gzip:          13,800 bytes (86.2% reduction)
Pretty + brotli:          11,500 bytes (88.5% reduction)
Minified + brotli:        11,200 bytes (88.8% reduction)

The difference between minified+compressed and pretty+compressed:
  gzip:   400 bytes  (0.4% of original)
  brotli: 300 bytes  (0.3% of original)

Gzip and brotli are extremely effective at compressing JSON because JSON is highly repetitive — the same field names appear in every object in a list, the same structural characters appear in predictable patterns. Compression algorithms exploit this repetition far more efficiently than whitespace removal does. Minification without compression reduces payload by ~18%. Compression without minification reduces it by ~86%. You are doing the math wrong if minification is your first move.

Enabling gzip and brotli: the five-minute fix

Most web servers have compression support built in but disabled by default. Enabling it requires minimal configuration and has no downside for JSON API responses.

# nginx
gzip on;
gzip_types application/json text/plain application/javascript;
gzip_min_length 1024;  # don't compress tiny responses
gzip_comp_level 6;     # sweet spot between CPU and ratio

# For brotli (requires ngx_brotli module)
brotli on;
brotli_types application/json;
brotli_comp_level 6;

# Express.js (Node.js)
import compression from 'compression';
app.use(compression()); // handles both gzip and brotli via Accept-Encoding

# Next.js — compression is enabled by default in production
# Vercel — compression is handled at the edge, no config needed

Brotli is worth enabling alongside gzip. Modern browsers send Accept-Encoding: gzip, deflate, br and the server can serve brotli to browsers that support it. Brotli consistently achieves 15-20% better compression ratios than gzip on text data, at comparable decompression speeds. The compression happens once (or is cached at the CDN); the decompression happens on every client.

When minification does matter

Minification is not useless — it is just solving the wrong problem for API responses over HTTP. There are two cases where minification has genuine impact regardless of compression:

Inline JSON in HTML (script tags). JSON embedded directly in HTML — for server-side rendered initial state — cannot be compressed independently of the HTML document. Minifying this JSON reduces the total HTML size and matters for Time to First Byte. Next.js and similar frameworks do this with __NEXT_DATA__. Keep it small.

localStorage and sessionStorage. Browser storage has no compression layer. JSON stored in localStorage is stored as-is. For applications that cache significant amounts of data client-side, minifying before storage and optionally applying LZ-string compression can make the difference between staying under the 5-10MB per-origin limit and exceeding it.

// Compress before storing in localStorage
import LZString from 'lz-string';

function storeLarge(key: string, data: unknown) {
  const json = JSON.stringify(data); // already minified by stringify
  const compressed = LZString.compress(json);
  localStorage.setItem(key, compressed);
}

function retrieveLarge<T>(key: string): T | null {
  const compressed = localStorage.getItem(key);
  if (!compressed) return null;
  const json = LZString.decompress(compressed);
  return JSON.parse(json);
}

Streaming JSON for large payloads

For payloads above a few megabytes, the approach changes entirely. Buffering a multi-megabyte JSON response in memory, parsing the entire document, and then processing it is wasteful. The right tool is streaming JSON parsing.

// Server: stream a large array without buffering
import { Readable } from 'stream';

app.get('/api/large-export', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.write('[');

  let first = true;
  const cursor = db.collection('records').find().cursor();

  cursor.on('data', (doc) => {
    if (!first) res.write(',');
    res.write(JSON.stringify(doc));
    first = false;
  });

  cursor.on('end', () => {
    res.write(']');
    res.end();
  });
});

// Client: parse streaming JSON without loading it all into memory
// Use 'oboe' or 'stream-json' for streaming parse in Node.js
import StreamArray from 'stream-json/streamers/StreamArray.js';
import { pipeline } from 'stream/promises';

await pipeline(
  response.body,
  StreamArray.withParser(),
  async function* (source) {
    for await (const { value } of source) {
      await processRecord(value); // process one at a time
    }
  }
);

The threshold where JSON becomes the wrong format

Compression and streaming extend JSON's range significantly, but there are thresholds at which JSON stops being the right format entirely:

High-frequency real-time data (>1,000 msg/sec): At this frequency, JSON serialization and deserialization CPU cost accumulates. MessagePack is a binary format that is a superset of JSON's data model — it encodes the same data in 20-30% fewer bytes and parses 2-3x faster. The tradeoff is human-readability.

Schema-stable high-volume data: Protocol Buffers and Avro are schema-based binary formats that achieve dramatically better compression (60-80% reduction vs raw JSON) and dramatically faster serialization by eliminating field names from the payload entirely. When you have millions of records with the same shape, encoding the schema separately and the data compactly is a significant win.

Tabular analytics data: Apache Parquet and Arrow are columnar formats that compress and query tabular data far more efficiently than row-oriented JSON. A 1GB JSON export of analytics data might be 50MB in Parquet with dramatically faster query performance.

The practical threshold: below 10,000 records per second and below 10MB per response, JSON with brotli compression is the right choice. Above that, profile first, then consider binary alternatives. Most applications never hit that threshold, and the simplicity of JSON is worth a great deal in the applications that do not.

The actual bottleneck: network latency, not payload size

A final note that puts all of this in context. For most API responses, the dominant cost is not payload size — it is network latency. A 14KB compressed JSON response on a 50ms RTT connection takes ~51ms to receive. A 13.8KB minified+compressed response on the same connection takes ~50.8ms. The 200-byte difference rounds to noise.

The engineering time spent fine-tuning JSON minification would be better spent deploying to an edge location closer to users, optimizing database query latency, implementing HTTP caching headers correctly, or reducing the number of serial API calls in the critical path. Payload size matters at the margins; architecture matters everywhere.

Published May 25, 2026 · By the utili.dev Team