JSON Toolkit

Deja de comprimir JSON en el orden equivocado: minify, gzip y brotli, con mediciones

En resumen: Minificar recorta entre un 30 y un 50 % del JSON en bruto, pero gzip borra la mayor parte de esa ganancia. Cifras medidas con formas reales, más los cuatro casos en los que minificar empeora las cosas.

Intermedio~7 min de lecturaPublicado el 2026-10-31Actualizado el 2026-10-31
Compresión de JSON: comparación medida del tamaño de payload con minify, gzip y brotli

Conclusión: separa primero la compresión de transporte del recorte estructural. Si gzip o brotli aún no están activados, corrige la configuración del servidor: solo gzip -6 alcanza el 94,5 % y deja a minify un valor de apenas 0,9 puntos. El 46,3 % completo de minify solo cuenta cuando la factura de bytes se registra localmente: cuerpos de petición, los 6 MB de Lambda, el 1 MiB de Firestore, o JSON escrito en disco y en logs.

La insignia de "35 % ahorrado" de tu minificador favorito es honesta: mide bytes en bruto. Tu API funciona sobre gzip o brotli, y esos dos se comen exactamente la misma redundancia. Multiplica las dos cifras y obtendrás la respuesta equivocada.

1. Divide "compresión" en dos cosas distintas

Lo que la gente llama compresión de JSON son en realidad dos operaciones sin relación, y confundirlas es como empieza toda conclusión errónea.

minify (quitar espacios en blanco) gzip / brotli (compresión de transporte)
Naturaleza Una transformación del documento Una transformación del transporte
Salida Texto JSON aún válido y aún legible Binario; inutilizable hasta decodificarlo
Dónde se materializa la ganancia Disco, memoria, logs, límites de tamaño Solo en el cable y en las cachés
Tras decodificar Los bytes siguen siendo más pequeños Los bytes vuelven a su tamaño original
Quién lo controla Tu script de build, los argumentos de tu serializador Nginx, CDN, framework del servidor

Las dos últimas filas son las que importan. Los límites de plataforma se miden después de decodificar: el tope de 6 MB de payload de AWS Lambda, el tope de 1 MiB por documento de Firestore. Activar gzip no te compra ni un solo byte ahí, porque ambos cuentan el resultado decodificado.

2. minify elimina exactamente una cosa: los espacios en blanco estructurales

RFC 8259 permite espacios en blanco (space, \t, \n, \r) alrededor de cualquier carácter estructural. Nada más en un documento JSON es opcional: los nombres de clave son datos, los números conservan cada dígito y el contenido de las cadenas es byte a byte intocable.

Eso reduce toda la operación a una línea:

const minified = JSON.stringify(JSON.parse(pretty));
// RFC 8259 permite espacios en blanco solo alrededor de caracteres estructurales,
// así que los espacios dentro de una cadena son datos, no formato
JSON.stringify(JSON.parse('{"note": "two spaces"}'));
// {"note":"two spaces"} - ambos espacios internos se conservan

Merece la pena señalar tres operaciones que se archivan bajo "compresión" pero que en realidad modifican el documento: eliminar claves cuyo valor es null, redondear decimales y reordenar claves. Cambian el documento en lugar de reformatearlo, así que cualquier herramienta que anuncie "compresión profunda" necesita la bendición de tu consumidor antes de que la actives.

Como minify solo quita espacios en blanco, su rentabilidad depende casi por completo de cuánta sangría hay en tu archivo. Espera verlo aparecer una y otra vez en las tablas siguientes.

3. La medición: cuatro documentos, tres niveles de gzip y brotli

Entorno: Node v22.13.1, zlib.gzipSync en nivel 1 / 6 / 9, brotliCompressSync con su calidad por defecto (11). Las cuatro muestras cubren deliberadamente las formas que realmente envías: 50 pedidos anidados (estructura profunda, claves repetidas), una configuración de 6 niveles (claves largas, valores cortos), un array de logs de 200 líneas (cadenas largas, casi nada de sangría) y el media-manifest.json real de este sitio (forma mixta, cadenas con muchas URL).

Paso uno: bytes en bruto.

Muestra pretty (2 espacios) pretty (4 espacios) tras minify ahorro de minify
50 pedidos anidados 29.551 B 40.761 B 15.881 B 46,3 %
Configuración de 6 niveles 25.450 B 38.012 B 11.679 B 54,1 %
Array de logs de 200 líneas 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 %

De aquí sale la insignia de "35 % ahorrado", y no miente. El problema aparece en el paso dos.

Paso dos: comprimir cada variante y preguntar cuánto añade todavía minify.

Muestra sobre gzip -1 sobre gzip -6 sobre gzip -9 sobre brotli
50 pedidos anidados 21,0 % 17,0 % 8,9 % 10,6 %
Configuración de 6 niveles 74,2 % 59,1 % 58,0 % 56,4 %
Array de logs de 200 líneas -0,7 % -0,4 % 0,6 % -0,3 %
media-manifest.json 1,5 % 1,0 % 1,0 % 1,6 %

El mismo documento de pedidos anidados que pierde el 46,3 % de sus bytes en bruto conserva solo el 17,0 % de esa ganancia una vez que gzip -6 entra en juego. El array de logs se vuelve directamente negativo: minificar y luego comprimir produce un resultado ligeramente mayor que comprimir el original pretty. Que sea negativo no es un bug, es exactamente lo que deberías esperar, y la sección siguiente explica por qué.

Paso tres: reducción total frente al documento pretty original, la cifra que realmente viaja por el cable.

Muestra solo gzip -6 minify + gzip -6 minify + brotli
50 pedidos anidados 94,5 % 95,4 % 96,9 %
Configuración de 6 niveles 96,0 % 98,4 % 99,0 %
Array de logs de 200 líneas 89,9 % 89,9 % 95,0 %
media-manifest.json 83,5 % 83,6 % 86,1 %

4. Por qué gzip se come el mérito de minify

gzip es LZ77 seguido de codificación Huffman. El movimiento central de LZ77 es "mira hacia atrás en una ventana de 32 KB, encuentra una secuencia repetida, sustituye con un puntero (distancia, longitud)". La sangría es el objetivo más fácil posible para ese movimiento: cada nivel de anidamiento emite una secuencia idéntica de espacios, y docenas de líneas adyacentes repiten la misma combinación. Huffman luego asigna códigos cortos a los símbolos de alta frecuencia, así que un patrón como \n más ocho espacios, que puede repetirse mil veces en un solo archivo, acaba costando dos o tres bits.

Así que los bytes que minify borra son precisamente los bytes que el compresor habría colapsado casi gratis. Hacerlo tú mismo significa rehacer un trabajo que el compresor ya hace excepcionalmente bien, y tu 46 % se desploma al 17 %. Esa es toda la historia detrás de la tabla anterior.

El resultado negativo del array de logs merece su propia explicación. Esa muestra está compuesta casi por completo de cadenas largas (líneas de log, URL, base64), lo que significa pocas repeticiones y alta entropía: poca redundancia que quitar de entrada al compresor. Los saltos de línea de la versión pretty resultan dar al emparejador límites estables. Elimínalos y LZ77 pierde esos anclajes: en esta muestra, comprimir el original pretty con gzip -1 produce 1.508 bytes, mientras que minificar y luego comprimir produce 1.518 bytes. Diez bytes peor.

Ordenadas por magnitud, las palancas se alinean así:

Palanca Ahorro frente al documento pretty original (pedidos anidados) Coste
minify 46,3 % (solo bytes en bruto) Se pierde la legibilidad; requiere un paso de build
gzip -6 94,5 % Algo de CPU del servidor; configuración casi nula
minify + gzip -6 95,4 % Las dos anteriores, sumadas
minify + brotli 96,9 % Requiere HTTPS y el módulo brotli; precomprimir los assets estáticos

El salto de 0 a 94,5 % es obra de gzip. El salto de 94,5 % a 95,4 %, nueve décimas de punto, es obra de minify. Esta tabla zanja dónde debe ir tu esfuerzo.

5. El nivel de compresión es la variable oculta

Cualquier argumento que asuma un único nivel de gzip fijo se apoya en arena, porque el nivel cambia directamente el valor marginal de minify:

Ajuste de transporte Bytes extra que minify aún ahorra (pedidos anidados)
gzip -1 (gzip_comp_level por defecto en Nginx) 21,0 %
gzip -6 (por defecto en la mayoría de las bibliotecas estándar) 17,0 %
gzip -9 8,9 %
brotli (calidad por defecto) 10,6 %

Cuanto más débil sea tu compresión de transporte, más rentable es minify. Eso te da una regla práctica: en una instalación de Nginx estándar donde gzip_comp_level sigue en 1, minify te entrega un 21 %. Una vez que lo has ajustado a 9 o te has pasado a brotli, baja al 9-11 % y normalmente puedes prescindir de él.

Antes de discutir en un sentido u otro, mide en qué escalón estás realmente:

# Solicita la misma respuesta con tres valores de Accept-Encoding y compara lo que llega
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

Tres números responden tres preguntas a la vez: ¿está la compresión siquiera activada, en qué nivel, y cuánto margen queda para minify? Si identity y gzip vuelven casi idénticos, la compresión no está activa en absoluto, y lo que hay que arreglar es Nginx, no tu JSON:

gzip on;
gzip_comp_level 6;
gzip_min_length 256;      # omite respuestas diminutas, solo crecen
gzip_types application/json application/javascript text/css;

6. Cuándo minify compensa de verdad

Una prueba lo decide: ¿los bytes que ahorraste quedarán registrados en algún sitio, o chocarás con un límite que cuenta bytes en bruto? Si la respuesta es sí, se materializa el 46 % completo. Si la respuesta es "solo existen en el cable", entonces 17 % es su valor real.

Primero, los cuerpos de petición HTTP. La mayoría de los servidores no descomprimen los cuerpos de petición, y gzip está configurado casi exclusivamente para respuestas. El JSON que envías con POST normalmente no tiene ninguna protección de capa de transporte, así que los ahorros de minify llegan a precio completo. Lo mismo vale para callbacks de webhook, telemetría MQTT y frames de WebSocket que nunca negociaron permessage-deflate.

Segundo, canales con un techo de bytes duro, y todos ellos cuentan bytes decodificados:

Canal Límite ¿Ayuda gzip?
Payload síncrono de AWS Lambda 6 MB No, se mide después de decodificar
Documento único de Firestore 1 MiB No
Mensaje único de Kafka (por defecto) 1 MB No, la compresión del broker es otra capa
Cuota de localStorage ~5 MB (contada en caracteres) No
URL de petición GET Depende del servidor, 8 KB es lo común No

En esta categoría minify no es una optimización, es la diferencia entre una petición que se envía y otra que falla. Incrustar JSON en una query string de URL es el caso más extremo: las URL nunca se comprimen y sí tienen un tope de longitud, así que minifica primero y luego decide si necesitas base64.

Tercero, JSON escrito en disco o en logs. Los logs y los índices almacenan bytes literales sin ninguna capa de transporte de por medio. Hay una advertencia que desconcierta a la gente: minify solo ayuda si de entrada escribías JSON pretty. Esa muestra de logs de 200 líneas solo ahorró un 4,1 %, porque sus líneas ya eran compactas y la sangría representaba casi nada del archivo. A la inversa, si tu logger llama a JSON.stringify(obj, null, 2), quitar el tercer argumento es dinero gratis.

Una última nota: cuando los bytes en bruto realmente aprietan, los nombres de clave suelen ser una palanca mayor que la sangría. En media-manifest.json, sustituir sus 14 claves únicas por k0 a k13 lleva la forma minificada de 21.222 B a 18.887 B, otro 11,0 %. Comprimida, esa ventaja restante se reduce al 2,4 % con gzip -6 y al 1,3 % con brotli. Mismo mecanismo que todo lo anterior: tanto la sangría como los nombres de clave son información estructural repetida, y el compresor se cobra el mismo dividendo en ambos casos.

7. Cuatro casos en los que sale mal

No minifiques archivos que versionas. Cambia "qty": 2 por 3 y el archivo pretty muestra una línea limpia en git diff; el archivo minificado reescribe la línea entera, y con cientos de campos no encontrarás el cambio en absoluto. Es la misma mecánica que se trata en Cómo resolver conflictos de merge en JSON: las herramientas de diff operan línea a línea, así que el JSON de una sola línea convierte cada edición en "toda la línea difiere". Mantén los archivos pretty en el repositorio y minifica solo en la salida del build o al escribir en almacenamiento.

No comprimas respuestas diminutas. Una respuesta que contiene solo {"ok": true} mide 16 bytes una vez añadida la sangría, y 36 bytes después de gzip -6: veinte bytes más grande, porque el diccionario y las cabeceras de gzip ocupan espacio. Esto no es teórico; es exactamente por lo que existe gzip_min_length. Su valor por defecto en Nginx, 20 bytes, es demasiado bajo para producción, donde 256 es el suelo habitual.

No esperes un análisis más rápido. Escalando el mismo conjunto de pedidos a 4.000 registros, el documento pretty mide 2.374.378 bytes frente a 1.282.358 minificado, un 46 % menos, y aun así la mediana del tiempo de JSON.parse solo pasa de 5,7 ms a 5,0 ms, aproximadamente un 12 % más rápido. Reducir el tamaño a la mitad no reduce el tiempo a la mitad, porque el analizador gasta la mayor parte de su presupuesto en cortar cadenas y convertir números; saltarse espacios en blanco es incidental. Lo que realmente determina el coste de análisis es el número de claves y el contenido de las cadenas, algo que Análisis, renderizado y streaming de JSON grande desglosa en detalle.

No lo apliques retroactivamente a un servicio que ya corre brotli o gzip -9. La rentabilidad cae al 9-11 %, mientras que el coste, la pérdida permanente de legibilidad al depurar, se queda. Comprueba en cambio si gzip_types cubre siquiera application/json. Muchos sitios activan gzip y solo comprimen text/html, dejando el JSON completamente sin comprimir; en esa situación minify no es el paso uno, arreglar la configuración sí.

8. Tabla de decisión: qué paso va primero

Tu situación Haz esto primero Dónde queda minify
Sin compresión de transporte en absoluto Configurar gzip_types en Nginx o activar la autocompresión de la CDN Omitirlo; nueve décimas partes de la ganancia se evaporan
Corriendo gzip -1 (por defecto en Nginx) Subir gzip_comp_level a 6 Segundo, vale un 21 %
Ya en brotli o gzip -9 Auditar claves largas, añadir paginación o recortar campos Omitirlo; solo queda un 9-11 %
Chocando con los 6 MB de Lambda o el 1 MiB de Firestore Minificar directamente; cuenta el 46 % completo Primero
Cuerpos POST, webhooks, MQTT Minificar directamente; el transporte rara vez aplica aquí Primero
Archivos de configuración que van a Git Mantenerlos pretty Solo en la salida del build
Logs, telemetría, volcados offline Quitar el argumento null, 2 de tu logger Ese es el paso uno

La regla en una frase: mira qué balance de bytes intentas reducir: uno registrado localmente (disco, cuota, límite de plataforma, cuerpo de petición) o uno que solo existe en el cable. Local primero significa minify; solo en el cable significa activar brotli o gzip primero.

En la práctica, ejecuta el bucle de curl anterior para confirmar que la compresión de transporte está viva, luego pasa una muestra por la herramienta JSON Minify y lee su cifra de ahorro. Si esa cifra supera el 40 % mientras curl informa que identity y gzip tienen casi el mismo tamaño, tu cuello de botella es la configuración del servidor, no el JSON. Si las dos cifras de curl ya están cerca, a este documento le queda poco que dar y deberías recortar campos o paginar en su lugar, el tema de Fundamentos del diseño de contratos de API JSON.

¿El JSON minificado sigue siendo JSON válido?

Sí. minify solo elimina los espacios en blanco que RFC 8259 permite alrededor de los caracteres estructurales y nunca toca el contenido de las cadenas, así que JSON.parse produce un valor idéntico al original. La única víctima es la legibilidad humana.

¿brotli o gzip?

Sobre la misma muestra minificada, brotli ahorra un 96,9 % y gzip -6 un 95,4 %, una brecha estrecha. Usa gzip -6 para respuestas dinámicas y mantén baja la CPU, y reserva brotli para assets estáticos servidos por HTTPS que puedas precomprimir en archivos .br.

Minifiqué mi JSON pero la API no se volvió más rápida. ¿Por qué?

Comprueba la respuesta con curl -sI en busca de una cabecera Content-Encoding: gzip. Muchas configuraciones de Nginx dejan gzip_types en text/html a solas, así que el JSON nunca quedó cubierto, y menos de una quinta parte de tu 46 % llegó al cable.

¿Los archivos de configuración deben versionarse minificados?

No. Las herramientas de diff comparan línea a línea, así que el JSON de una sola línea convierte cada edición en "toda la línea cambió", y resolver conflictos cuesta mucho más que los bytes que ahorraste. Mantén las versiones pretty en el repositorio y minifica en tiempo de build.

¿Qué tamaño debe tener una respuesta para que gzip compense?

El umbral habitual es de 256 bytes a 1 KB. Una respuesta tan pequeña como {"ok": true}, 16 bytes con sangría, crece a 36 bytes con gzip -6, porque el diccionario y las cabeceras cuestan más de lo que recuperan. Eso es precisamente lo que controla gzip_min_length.