Compresión de JSON: técnicas y análisis de rendimiento

30 Jun 2011

Introducción

Ayer comentaba con un colega como el formato JSON se ha convertido en el estándar de facto a la hora de intercambiar información entre servicios web desbancando definitivamente al viejo XML. En esta línea, otras plataformas importantes como Twitter o Facebook anunciaban también hace algunos meses que sus respectivas APIs trabajarían únicamente con este estándar.

La razón del éxito de este formato viene dada por tres factores:

  • Implementación nativa con Javascript: este hecho, crucial para manejar peticiones AJAX fácilmente, ha supuesto incluso la creación de bibliotecas y snippets para convertir directamente todo XML en JSON.
  • Sencillez a la hora de estructurar la información.
  • Legibilidad de código mucho más natural: en lugar de complicados árboles de interminables etiquetas en XML, la notación de clave-valor JSON resulta mucho más humana y manejable.

El problema

Sin embargo, precisamente por la naturaleza de los datos, uno de los mayores inconvenientes que podemos encontrar es el tamaño desmedido de los objetos resultantes. Si tenemos en cuenta de que estamos hablando de un formato nacido para la web, un objeto excesivamente pesado se vuelve ineficiente y poco práctico.

Al tratarse de un sistema para codificar colecciones de datos (libros, mensajes, música) encontramos que las claves siempre se repiten en un número proporcional al de elementos que contiene. Tomemos por ejemplo este código obtenido del API de OpenLibra:

[
  {
    "ID":"1067",
    "title":"Matem\u00e1tica\u2026 \u00bfEst\u00e1s Ah\u00ed? Ep.3,14",
    "author":"Adri\u00e1n Paenza",
    "publisher":"Siglo XXI Editores Argentina S.A.",
    "publisher_date":"2007",
    "pages":"118",
    "language":"spanish",
    "url_details":"http:\/\/www.etnassoft.com\/biblioteca\/matematica-estas-ahi-ep-3-14\/",
    "url_download":"http:\/\/books.openlibra.com\/packed\/",
    "cover":"http:\/\/books.openlibra.com\/covers\/2011\/06\/Matematica_estas_ahi_Ep_314-332x461.jpg",
    "rating":"4",
    "num_comments":"0",
    "categories":[
      {
        "category_id":"206",
        "name":"Ciencia",
        "nicename":"ciencia"
      },
      {
        "category_id":"208",
        "name":"Matem\u00e1ticas",
        "nicename":"ciencia_juegos-ciencia"
      }
    ],
    "tags":[
      {
        "tag_id":"296",
        "name":"matem\u00e1ticas",
        "nicename":"matematicas"
      },
      {
        "tag_id":"297",
        "name":"matem\u00e1ticas recreativas",
        "nicename":"matematicas-recreativas"
      }
    ]
  },
  {
    "ID":"836",
    "title":"Matem\u00e1tica... \u00bfEst\u00e1s ah\u00ed? Ep. 2",
    "author":"Adri\u00e1n Paenza",
    "publisher":"Universidad Nacional de Quilmes",
    "publisher_date":"2006",
    "pages":"235",
    "language":"spanish",
    "url_details":"http:\/\/www.etnassoft.com\/biblioteca\/matematica-estas-ahi-2\/",
    "url_download":"http:\/\/books.openlibra.com\/packed\/",
    "cover":"http:\/\/books.openlibra.com\/covers\/2011\/05\/matematica_estas_ahi_episodio_2.jpg",
    "rating":"4",
    "num_comments":"0",
    "categories":[
      {
        "category_id":"206",
        "name":"Ciencia",
        "nicename":"ciencia"
      },
      {
        "category_id":"208",
        "name":"Matem\u00e1ticas",
        "nicename":"ciencia_juegos-ciencia"
      }
    ],
    "tags":[
      {
        "tag_id":"296",
        "name":"matem\u00e1ticas",
        "nicename":"matematicas"
      },
      {
        "tag_id":"297",
        "name":"matem\u00e1ticas recreativas",
        "nicename":"matematicas-recreativas"
      }
    ]
  },
  {
    "ID":"776",
    "title":"Matem\u00e1tica... \u00bfEst\u00e1s ah\u00ed?",
    "author":"Adri\u00e1n Paenza",
    "publisher":"Universidad Nacional de Quilmes",
    "publisher_date":"2005",
    "pages":"119",
    "language":"spanish",
    "url_details":"http:\/\/www.etnassoft.com\/biblioteca\/matematica-estas-ahi\/",
    "url_download":"http:\/\/books.openlibra.com\/packed\/",
    "cover":"http:\/\/books.openlibra.com\/covers\/2011\/05\/matematica_estas_ahi.jpg",
    "rating":"5",
    "num_comments":"0",
    "categories":[
      {
        "category_id":"206",
        "name":"Ciencia",
        "nicename":"ciencia"
      },
      {
        "category_id":"208",
        "name":"Matem\u00e1ticas",
        "nicename":"ciencia_juegos-ciencia"
      }
    ],
    "tags":[
      {
        "tag_id":"296",
        "name":"matem\u00e1ticas",
        "nicename":"matematicas"
      },
      {
        "tag_id":"297",
        "name":"matem\u00e1ticas recreativas",
        "nicename":"matematicas-recreativas"
      }
    ]
  }
]

Se trata de una colección (en este caso de libros con un mismo autor) compuesta de 3 elementos. Si estudiamos los datos, vemos claramente cómo las claves siempre se repiten en cada uno de ellos: ID, title, author, publisher, …

Si el objeto obtenido en lugar de contener solo unos pocos elementos estuviese compuesto por varios miles (pensemos en una petición al API de IMDB para consultar todas las películas en las que aparece Jorge Sanz), el tamaño final puede ser demasiado grande como para conseguir tiempos de carga óptimos.

En esos casos en los que este formato lo utiliza una aplicación de forma interna y podemos sacrificar parte de su legibilidad, es cuando podemos aplicar un criterio de compresión que consiga reducciones del margen del 70% con respecto al original.

Algoritmo o método básico de compresión

Tomemos un ejemplo más sencillo de objeto:

[{
  name : "Andrea",
  age : 31,
  gender : "Male",
  skilled : true
}, {
  name : "Eva",
  age : 27,
  gender : "Female",
  skilled : true
}, {
  name : "Daniele",
  age : 26,
  gender : "Male",
  skilled : false
}]

Aquí vemos repetidas las claves name, age, gender y skilled. El algoritmo de compresión más básico para este objeto sería el extraer todas las claves repetidas y almacenarlas en un índice situado en la cabecera para más tarde recomponer los datos. El resultado sería algo así:

[
  [
    "name",
    "age",
    "gender",
    "skilled"
  ],
  [
    "Andrea",
    31,
    "Male",
    true
  ],
  [
    "Eva",
    27,
    "Female",
    true
  ],
  [
    "Daniele",
    26,
    "Male",
    false
  ]
]

Como podemos comprobar, al eliminar las claves de cada elemento de la colección disminuimos el tamaño del objeto drásticamente. Las claves permanecen siempre en un índice superior que nos serviría para parsear el resultado más adelante.

Reorganizando la información mediante templates

Otra forma de optimizar el resultado es mediante la creación de templates. Pensemos en una aplicación gráfica que almacena puntos y rectángulos en un objeto JSON:

[
  {
    // This is a point
    "x":100,
    "y":100
  },
  {
    // This is a rectangle
    "x":100,
    "y":100,
    "width":200,
    "height":150
  },
  {
    // an empty object
  },
    ... // thousands more
]

De nuevo, el objeto presenta un tamaño mayor al necesario al repetirse las claves x, y, width y height tantas veces como elementos contenga la colección. Estos valores realmente solo necesitarían ser almacenados una vez por cada tipo de objeto:

{
  "templates": [ ["x", "y"], ["x", "y", "width", "height"] ],
  "values": [
    { "type": 1, "values": [ 100, 100 ] }, { "type": 2, "values": [100, 100, 200, 150 ] }, {} ]
}

En el ejemplo anterior, hemos definido dos tipos o plantillas: puntos y rectángulos. Cada objeto de la colección ahora se hace corresponder con uno de ellos mediante un puntero ganado así espacio. Cada una de las plantillas que componen el índice es lo que denominaríamos templates.

Pero si observamos el resultado anterior, rápidamente vemos que se puede ir más allá eliminando la clave-valor type para pasar a indicarla por ejemplo, como primer valor de cada elemento. El resultado sería el siguiente:

{
    "templates": [ [1, "x", "y"], [2, "x", "y", "width", "height"] ],
    "values": [
      { "values": [ 1,  100, 100 ] }, { "values": [2, 100, 100, 200, 150 ] }, {} ]
}

La reducción ahora es mayor: el primer valor de cada elemento determina su tipo, con lo que hemos eliminado todos los elementos redundantes.

Herramientas de compresión

Actualmente existen varias herramientas que permiten este tipo de compresión. Las más destacadas son:

  • HPack: pertenece al primer tipo de compresión que hemos mencionado y obtiene ratios del orden del 70% con respecto al original. Esta herramienta presenta 4 niveles de compresión que podemos escoger según nuestras necesidades.
  • CJSON: este algoritmo correspondería al segundo ejemplo que hemos expuesto y alcanza ratios del orden del 60%.

Además de las herramientas anteriores, con los objetos JSON pueden también aplicarse técnicas de minificado y compresión estándar del mismo modo en que se hace con otros archivos javascript u hojas de estilo CSS. Para profundizar más en este aspecto, se recomienda leer el artículo Minificado y ofuscación de código en Javascript.

Métricas sobre rendimiento

Alex, de Web Resource Optimization ha elaborado unas métricas que analizan el rendimiento de cada una de las técnicas descritas anteriormente. Para el estudio, se han tomado 5 ficheros JSON con tamaños que varian desde los 50Kbs a 1Mb. Cada fichero seha sometido a las siguientes transformaciones:

  • JSON sin modificar: el fichero tal cual.
  • JSON minificado: se han eliminado los espacios en blanco y los saltos de línea.
  • Compresión de JSON con CJSON.
  • Compresión de JSON con HPack.
  • Gzipped JSON: preservando la estructura original.
  • Gzipped JSON y minificado.
  • Gzipped JSON más compresión CJSON.
  • Gzipped JSON más compresión HPack.

Los resultados obtenidos se muestran en la siguiente tabla:

JSON 1 JSON 2 JSON 3 JSON 4 JSON 5
JSON Original 52966 104370 233012 493589 1014099
Minificado 33322 80657 180319 382396 776153
Compresión con CJSON 24899 48605 108983 231760 471230
Compresión con HPack 5727 10781 23162 49099 99575
Gzipped 2929 5374 11224 23167 43550
Gzipped JSON y minificado 2775 5035 10411 21319 42083
Gzipped JSON y compresión con CJSON 2568 4605 9397 19055 37597
Gzipped JSON y compresión con HPack 1982 3493 6981 13998 27358

 

La gráfica elaborada a partir de estos datos en cuanto al ratio de compresión habla por si sola:

El análisis de los resultados indica que:

  • La minificación es mucho más eficiente en ficheros pequeños.
  • Para serie de datos grandes y muy grandes, la minificación tiene eficacia constante.
  • La compresión de los ficheros utilizando los algoritmos anteriores tienen una eficiencia similar independientemente del tamaño original.
  • CJSON es menos eficiente que HPACK.
  • La compresión GZIP no supone ninguna mejora con respecto al original.

Conclusión

La compresión de los objetos JSON demuestra un ahorro significativo en el tamaño final del fichero. Estas técnicas sacrifican la legibilidad del estándar para ofrecer un mayor rendimiento a la hora de intercambiar información entre aplicaciones. Será cada proyecto el que finalmente, dependiendo de sus necesidades, el que determine si conviene utilizar algunas de las técnicas descritas anteriormente.

Más:

{7} Comentarios.

  1. acid

    como aporte, aunque sólo se nota si el Json con muchas variables (+25000)…

    si tienes muchos arrays repetidos:
    [1,’2′,3,’4′],
    [1,’2′,3,’4′],
    [1,’2′,3,’4′],
    [1,’2′,3,’4′],

    el navegador a se queda un poco tonto por las miles de variables; y lo mejor es ponerlo como una cadena y luego explotarlo:
    ‘1,2,3,4’,
    ‘1,2,3,4’,
    ‘1,2,3,4’,

    de este modo reduces el número de variables considerablemente, aunque tienes que tener en cuenta que tienes que explotar la cadena en cada operación, pero a veces es la única solución antes de que el navegador se quede frito.

    Por lo visto es un técnica bastante extendida en los juegos JS; ahora mismo no recuerdo el enlace en el que lo explican mejor 😛

    • Carlos Benítez

      Buenas; efectivamente esa técnica está sacada directamente del desarrollo de videojuegos. Todavía recuerdo perfectamente como elaborábamos los mapas para los juegos de plataformas a partir de arrays interminables 😉

      Hoy día, con los juegos en 3D (o pseudo 3D), cuando tenemos que renderizar vectores, incluimos los puntos en matrices: para optimizar la notación, la mejor forma de hacerlo es mediante cadenas literales que luego se dividen (explotan) para ir extrayendo las coordenadas; con esto evitamos la creación de variables intermedias que luego el sistema tiene que purgar de memoria (en Javascript, por poner un ejemplo, este proceso tiene un coste significativo). Es también frecuente que esos valores se encuentren a su vez en alguna base distinta a la decimal para ahorrar aún más espacio.

      Me acaban de entrar ganas de programar un plataformas tipo Contra en Javascript… 🙂

    • Carlos Benítez

      Buenas; efectivamente esa técnica está sacada directamente del desarrollo de videojuegos. Todavía recuerdo perfectamente como elaborábamos los mapas para los plataformas a partir de arrays interminables 😉

      Hoy día, con los juegos en 3D (o pseudo 3D), cuando tenemos que renderizar vectores, incluimos los puntos en matrices: para optimizar la notación, la mejor forma de hacerlo es mediante cadenas literales que luego se dividen (explotan) para ir extrayendo las coordenadas; con esto evitamos la creación de variables intermedias que luego el sistema tiene que purgar de memoria (en Javascript, por poner un ejemplo, este proceso tiene un coste significativo). Es también frecuente que esos valores se encuentren a su vez en alguna base distinta a la decimal para ahorrar aún más espacio.

      Me acaban de entrar ganas de programar un Contra en Javascript… 🙂

  2. Jordi Rivero

    Buenas, yo ando probando MessagePack (http://msgpack.org/), un formato binario que se presenta como alternativa a json y que tiene librerias para todos (los que yo utilizo) o casi todos los lenguajes.

    No he visto ninguna comparativa en nuestro idioma, puede ser un buen articulo 😉

    Saludos

    • Carlos Benítez

      Tienes razón; MessagePack es una herramienta muy interesante y hay poca cosa en castellano.

      Echaré un vistazo a ese artículo 😉

      Saludos!

  3. Israel

    Hola.
    No entiendo el último ejemplo, ¿por qué ha desaparecido la “x” e “y” del rectángulo? ¿Los valores no deberían ser “0” y “1” respectivamente en vez de “1” y “2”?

    Muchas gracias.

    • Carlos Benítez

      Si; efectivamente había un pequeño error: “0” y “1” deberían ser los valores correctos; pero no es natural para el lenguaje hablar de un tipo “0” (JSON hace mucho hincapié en que sea legible), por eso he modificado el índice de “x” e “y” para que se correspondan con 1 y 2.
      Digamos que al final, lo que tenemos son las referencias correspondientes a los valores almacenados en el template. CJSON se encargaría después de relacionar correctamente cada valor con el elemento de la plantilla adecuada.

      Ya está corregido; gracias por el aviso!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *