Minificado y ofuscación de código en Javascript

26 Abr 2011

Introducción

Tras la pasada conferencia en el Espacio CAMON Madrid sobre la evolución del lenguaje Javascript, tratamos algunos aspectos que quedaron un poco en el aire por falta de tiempo.

Uno de aquellos temas que más preguntas despertaron tras la charla fue la minificación y ofuscación del código. Mi postura fue posicionarme en contra de este tipo de prácticas y de ahí las dudas posteriores.

Trataré con esta entrada de dejar clara mi opinión al respecto a la vez que nos introducimos en lo que ambas técnicas conllevan y las herramientas más apropiadas para llevarlas a cabo.

Javascript como lenguaje interpretado

Como comentamos durante el evento, Javascript es un lenguaje interpretado: esto quiere decir que se diferencia radicalmente de aquellos otros pertenecientes a la familia C que requieren de un compilado previo a su ejecución.

En esencia, tenemos que recordar que Javascript pertenece a dicha familia de lenguajes por su proximidad sintáctica y estructura general orientada a objetos. Se diferencia, sin embargo, en conceptos como los prototipos en lugar de clases, el tipado blando y el ya mencionado interpretado en tiempo de ejecución.

El proceso de compilado, hay que recordar que busca convertir un código introducido por un desarrollador (u otro programa) en un lenguaje que la máquina pueda interpretar. Este proceso, suele además, mejorar el diseño original mediante algunas técnicas básicas que pasamos a enumerar a continuación para explicarlas un poco más en detalle.

Resumiendo, las ventajas que me interesa destacar de un lenguaje compilado para oponerlas a Javascript son:

Reducción del código mediante el renombrado de variables.
Optimización del rendimiento mediante un refactorizado básico.
Detección de errores en fase de diseño.

Veamos la primera característica. Es trivial que un compilador modifique el código entrante a nivel por ejemplo de variables. Esto quiere decir que el programa tomará cada uno de los nombres declarados y lo reducirá (sustituirá) por su mínima expresión buscando el menor tamaño del archivo final.

En Javascript, este proceso sería como sigue.

Partimos de este código:

var mySuperVariable = 'Hello';
var mySuperVariableTwo = 'World';
 
var resultOfConcatenation = mySuperVariable + mySuperVariableTwo;

Y nuestro supuesto compilador, devolvería:

var a = 'Hello';
var b = 'World';
var c = a + b;

De forma interna, se cambiarían los nombres de las variables por su versión reducida para ahorrar espacio.

En este mismo proceso, el compilador también eliminará comentarios, espacios y líneas en blanco que no ofrecen ningún valor real al código e incrementan el tamaño final del archivo.

Además de minificar, un buen compilador puede ser capaz de realizar tareas básicas de refactorizado. Esto quiere decir que si detecta determinadas estructuras, puede reescribir aquellas partes de código según unos patrones prefijados que se han demostrado más eficientes. El resultado será una funcionalidad idéntica a la original pero optimizada.

En Javascript, podríamos reflejar esto con un bucle ordinario.

Tomaríamos por ejemplo el siguiente código:

for( var x = 0; x < 1000000; x++){
  // Something interesting here...
};

A partir del mismo, nuestro supuesto compilador podría reinterpretarlo del siguiente modo:

var i = 1000000;
while(i--){
    // Something interesting here...
}

NOTA: El último ejemplo se ha demostrado ser mucho más rápido que un for ordinario. Para ver la comprobación, tenéis los tiempos de ejecución en el artículo: «Consola de Firebug al Detalle«.

Otra característica del compilador que no puede pasarse por alto es que detecta errores en la fase de diseño e impide compilar si el código no ha validado previamente. En Javascript, al tratarse de un lenguaje con sistema de errores sileciosos, los archivos pueden ser cargados aunque no sean válidos; únicamente la ejecución se detendrá al encontrarse el intérprete con algún tipo de error.

Javascript se interpreta en tiempo de ejecución

Esto quiere decir que nuestro código es enviado tal cual al intérprete para que éste se encargue de ejecutarlo; sin paso previo alguno.

La consecuencia directa de esto es que todo el proceso de minificado, refactorizado y debug, debe realizarse a mano en fase de diseño por parte del equipo de desarrollo.

Como estamos hablando de archivos que se descargan en el cliente antes de ser ejecutados y que utilizan la potencia de la máquina para correr, debe ser interesante conseguir tanto el menor tamaño de archivo como la estructura más óptima. Por supuesto, a lo anterior hay que sumar el que no presente errores (ya sean de sintaxis o los famosos memory leaks).

Mientras que para el refactorizado y el debug contamos con herramientas como JSHint, para la minificación tenemos varías alternativas.

Reduciendo el tamaño de nuestros archivos Javascript

Para conseguir archivos más pequeños, disponemos de dos técnicas esenciales: minificación y ofuscación.

La diferencia entre ambos métodos es muy sútil y para muchos ambigüa, por lo que no está demás comentar cada una de ellas por separado.

Minificado

Entendemos por minificación aquel proceso que toma un código y lo reduce a su mínima expresión mediante la eliminación de comentarios, saltos de línea y espacios en blanco innecesarios.

El resultado final del proceso es el código introducido originalmente pero compactado y, por supuesto, menos legible. El ratio de compresión alcanzado varía lógicamente dependiendo del fuente pero, como media, podemos hablar de una reducción en torno al 45-50%.

Existen multitud de herramientas online que facilitan este proceso. Las más conocidas son:

Free JavaScript Compressor
JavaScript Compressor Tool
UglifyJS JavaScript minification

Todas tienen un comportamiento similar: pegamos nuestro código y nos devuelven la versión reducida. Por lo general, estas herramientas nos informan tanto del tamaño inicial como el conseguido, pudiendo así calcular el ratio de compresión alcanzado.

Para mostrar un ejemplo de minificación, tomemos el siguiente código:

// is.js
 
// (c) 2001 Douglas Crockford
// 2001 June 3
 
// is
 
// The -is- object is used to identify the browser.  Every browser edition
// identifies itself, but there is no standard way of doing it, and some of
// the identification is deceptive. This is because the authors of web
// browsers are liars. For example, Microsoft's IE browsers claim to be
// Mozilla 4. Netscape 6 claims to be version 5.
 
var is = {
    ie:      navigator.appName == 'Microsoft Internet Explorer',
    java:    navigator.javaEnabled(),
    ns:      navigator.appName == 'Netscape',
    ua:      navigator.userAgent.toLowerCase(),
    version: parseFloat(navigator.appVersion.substr(21)) ||
             parseFloat(navigator.appVersion),
    win:     navigator.platform == 'Win32'
}
is.mac = is.ua.indexOf('mac') >= 0;
if (is.ua.indexOf('opera') >= 0) {
    is.ie = is.ns = false;
    is.opera = true;
}
if (is.ua.indexOf('gecko') >= 0) {
    is.ie = is.ns = false;
    is.gecko = true;
}

Y ahora, veamos su versión minificada:

var is={ie:navigator.appName=='Microsoft Internet Explorer',java:navigator.javaEnabled(),ns:navigator.appName=='Netscape',ua:navigator.userAgent.toLowerCase(),version:parseFloat(navigator.appVersion.substr(21))||parseFloat(navigator.appVersion),win:navigator.platform=='Win32'}is.mac=is.ua.indexOf('mac')>=0;if(is.ua.indexOf('opera')>=0){is.ie=is.ns=false;is.opera=true}if(is.ua.indexOf('gecko')>=0){is.ie=is.ns=false;is.gecko=true}

Para el caso anterior, los datos de salida serían:

– Ratio 1004 / 432 = 57%

La ventaja está clara: conseguimos reducir el tamaño del original más de su mitad. Esto, para grandes bibliotecas, puede suponer una reducción en el tiempo de carga a tener en cuenta.

Ofuscación

Además de lo anterior, la ofuscación introduce un nuevo matiz: la alteración literal del código mediante procesos de parseo y sustitución.

El resultado de una ofuscación es no sólo un código más compacto, sino diferente en sintaxis al original.

Tenemos de nuevo varias herramientas online que realizan este trabajo por nosotros, pero sin duda, una de las más conocidas por su eficacia es el PACKER de Dean Edwars.

El proceso es el siguiente, se crea una función maestra que recibe como argumentos una serie de valores que se corresponden con las cadenas literales a sustituir en el código original mediante expresiones regulares. Finalmente, el resultado es interpretado mediante un eval pasando así directamente al intérprete.

Un ejemplo de lo complejo del sistema lo podemos ver con un código simple:

var foo = 'Hello World';

Esto, tras el proceso de compresión, da lugar a lo siguiente:

eval(function(p,a,c,k,e,r){e=String;if(!''.replace(/^/,String)){while(c--)r[c]=k[c]||c;k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('0 1=\'2 3\';',4,4,'var|foo|Hello|World'.split('|'),0,{}))

Como podemos comprobar a simple vista, solo iniciar el proceso de compresión ya requiere de una serie algoritmos que pueden incluso aumentar el tamaño del original.

Obviamente, este método funciona correctamente con archivos grandes. Si volvemos a tomar el código que minificamos más arriba, el resultado tampoco es satisfactorio:

eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('g 1={4:2.7==\'h i j\',k:2.l(),5:2.7==\'m\',3:2.n.o(),p:8(2.9.q(r))||8(2.9),s:2.t==\'u\'}1.a=1.3.6(\'a\')>=0;b(1.3.6(\'c\')>=0){1.4=1.5=d;1.c=e}b(1.3.6(\'f\')>=0){1.4=1.5=d;1.f=e}',31,31,'|is|navigator|ua|ie|ns|indexOf|appName|parseFloat|appVersion|mac|if|opera|false|true|gecko|var|Microsoft|Internet|Explorer|java|javaEnabled|Netscape|userAgent|toLowerCase|version|substr|21|win|platform|Win32'.split('|'),0,{}))

De nuevo, no hemos reducido nada. Bueno, realmente si. Si miramos el ratio, tenemos lo siguiente:

– Ratio: 1004 / 674 = 0.6%

Algo insignificante y claramente inferior a lo que cabría esperar.

Pero si volvemos a realizar el experimento con un archivo aún mayor, como por ejemplo una bibliteca, si empezamos a obtener reducciones considerables.

Si tomamos el código de jQuery para desarrolladores (con comentarios, etc) y lo sometemos al test, obtenemos el siguiente ratio:

– Ratio 183148 / 52279 = 71%

Parece que ahora si es efectivo. Quizá tendríamos que tenerlo en cuenta a la hora de subir un script a un entorno crítico de producción.

Otras herramientas similares de ofuscado que tenemos a nuestra disposición son:

Javascript Obfuscator
Online Javascript Obfuscator

El proceso es similar a los anteriores: pegamos nuestro código y, tras pulsar un botón, se nos devuelve el mismo modificado.

Inconvenientes

Parece que ambos procesos son interesantes ya que reducen el tamaño de los archivos; sin embargo, como expuse al principio, no estoy a favor del uso de estos programas debido a los inconvenientes que encuentro en cada uno.

Ofuscación

Comienzo enumerando las desventajas del proceso de ofuscación ya que me resultan más evidentes:

El código final es completamente ilegible ya que el proceso modifica activamente su sintaxis.
– El hecho de tener que evaluar una expresión supone que el intérprete Javascript debe prepararlo antes de ejecutarlo.

El primer punto presenta dos problemas directos: la mantenibilidad del código y la dificultad para reutilizarlo por parte de la comunidad.

Cuando me refiero a la mantenibilidad del código, quiero decir que, en un escenario ideal, siempre existe una versión del script sin modificar y sobre la que se trabaja. Una vez completada una tarea, se vuelve a comprimir para enviar a servidor. Este proceso, sin embargo, no suele ser el real: en un entorno de desarrollo con multitud de programadores, es más frecuente de lo que parece el que el archivo original se pierda y tengamos que trabajar directamente sobre el comprimido. Obviamente, esto no se vuelve tarea fácil…

Al presentarse el código ofuscado, también estamos dificultando el que otros desarrolladores puedan leerlo y tomar soluciones del mismo para sus propios proyectos. Esto, más que un inconveniente real, sería dificultar la libre circulación del conocimiento que el Software Libre propone. Insisto con esto en que se trata más de una opinión personal que técnica.

Pero si que tenemos problemas técnicos cuando nos ponemos a medir el rendimiento de un código comprimido. Los más veteranos recordarán que las primeras versiones de jQuery (creo que hasta la 1.3), ofrecían la posibilidad de descarga ofuscada. Los análisis posteriores realizados por John Resig demostraron que, pese a que la biblioteca efectivamente se reducía en tamaño, el tiempo que tomaba el intérprete en descomprimir el archivo penalizaba más que el posible ahorro en el tiempo de carga. Tenéis algunas de sus conclusiones aquí.

Por lo tanto, a la hora de escoger entre una opción de ofuscación y minificado, habría que evaluar si compensa un mayor ratio de compresión con respecto al tiempo que el intérprete requiere para su descompresión.

Finalmente, otro punto que encuentro molesto es que no se pueden trazar posibles errores en un código ofuscado. Cuando hablamos de aplicaciones web, susceptibles de ser ejecutadas en un sin fin de plataformas y dispositivos, es interesante poder comprobar de una manera rápida las trazas que puede arrojar un error ocasional. Un código comprimido que falla no da información útil alguna.

Minificado

El proceso de minificado solo tiene para mi el inconveniente de que, si no se han alterado el nombre de las variables, dificulta de nuevo la lectura y mantenibilidad del código.

Y aquí hay que tener en cuenta una nueva consideración: pese a que es cierto que reducimos espacio, por lo general, éste es marginal.

A día de hoy contamos con infraestructuras de red poderosas y una cobertura de banda ancha superior al 99% del territorio en España y del 60% en el mundo. Estas conexiones permiten la descarga de paquetes de gran tamaño en cortos periodos de tiempo y, cuando hablamos de archivos de tipo javascript, las reducciones de las que estamos hablando son del orden de pocos Kbs.

Un fichero js no debería superar los 400Kbs; de hecho, tamaños mayores sugieren que, o bien el código no está optimizado (por ejemplo se trata de una librería muy pesada que usamos solo en parte), o bien estamos trabajando en una Intranet donde el tiempo de carga no es crítico.

En el mundo real, un script suele ocupar de media 85Kbs; para este tipo de ficheros, un ratio de compresión razonable, por ejemplo un 45%, solo supone reducir el tiempo de carga final en 40Kbs. Es decir, milisegundos para una conexión estándar. Si a esto sumamos que, gracias a las buenas prácticas, los archivos Javascript suelen cargarse en asíncrono, el tiempo se enmascara entre el renderizado del navegador y la reacción del usuario.

Por lo tanto, el proceso de minificación puede no suponer una mejora apreciable en cuanto a la experiencia del usuario mientras que si supone una dificultad añadida al desarrollador que accede al código fuente.

El argumento de la seguridad

Además de los motivos anteriores, una razón que muchos desarrolladores utilizan para justificar el ofuscado de un código es que este proceso proporciona una capa de seguridad extra contra intrusiones.

De hecho, queda patente que al hablar de un lenguaje interpretado, se permite a los hackers echar un vistazo a nuestro código en busca de errores o agujeros de seguridad que puedan ser explotados de una u otra forma. Ante esto, parece que ofuscando el código les ponemos las cosas más difíciles.

Nada más lejos de la realidad: ambos procesos de compresión (la minificación y el ofuscado), cuentan con sus inversos para devolver al código un aspecto muy próximo al original.

Descomprimiendo y reformateando un archivo Javascript

Efectivamente, para reformatear un código minificado, contamos con herramientas online muy interesantes que embellecen un código. Algunas de ellas son:

Javascript Beautifier
Vitzo Javascript Beautifier

Ambas herramientas permiten pegar un texto minificado y reformatearlo según la indentación deseada. La mayoría de los IDEs también cuentan con sus propias funciones para estos menesteres.

Para el caso de los códigos ofuscados con PACKER, tenemos el algoritmo inverso que devuelve un código Javascript puro aunque con las variables aún minificadas. La herramienta más utilizada en este aspecto es el Javascript unpacker and beautifier.

De nuevo, el uso es similar a los casos anteriores: pegar el código y copiar la salida reformateada.

Programas propietarios «todo terreno»

No suelo comentar nunca soluciones que no son gratuítas, online o directamente Software Libre. Sin embargo, en este caso, creo que puede resultar interesante comentar que existe una herramienta propietaria (de pago) que realiza un ofuscado total del código muy difícil de invertir y que puede ser útil para entornos críticos donde se precisa esa capa de seguridad extra.

La herramienta en cuestión se llama Javascript Obfuscator y puede encontrarse toda la información al respecto en su página oficial.

A su favor tiene un algoritmo con un alto ratio de compresión y una salida de código francamente compleja de invertir.

Conclusión

El hecho de que Javascript se trate de un lenguaje interpretado, implica que se deben realizar a manos tareas como la optimización, refactorizado y seguimiento de errores en fase de diseño.

Al tratarse de un lenguaje cliente, los archivos precisan ser descargados antes que interpretados. Esto hace que uno de los pasos finales de todo desarrollo sea la reducción del tamaño de los ficheros.

Dentro de este campo de la compresión, hemos visto que disponemos tanto de herramientas de minificado como de ofuscación, cada una de las cuales presenta sus ventajas e inconvenientes.

Finalmente, tras sopesar pros y contras, encuentro que sólo son interesantes en escenarios muy complejos donde realmente los tiempos de cargas sean críticos y justifiquen sacrificar tanto la legibilidad del código como un proceso de depurado o modificación.

Sin embargo, tal y como he argumentado, mi opinión en contra de estos procesos se basa en que, como desarrollador, me gusta poder ver el código fuente de un programa y entender de un solo vistazo como funciona, dónde están los errores y cuáles son los comentarios que sus desarrolladores originales introdujeron durante las fases de diseño. Además de esto, me gusta la idea de poder tomar aquellas partes que me parezcan interesantes para reutilizarlas en mis propios desarrollos sin tener que intuir qué parte hace qué cosa.

Como en casi todo, cada programador tendrá su propio criterio al respecto.

Más:

{8} Comentarios.

  1. @jonasanx

    Bastante interesante el articulo, honestamente yo era de los que pensaba que todo código JavaScript en un ambiente de producción debería ser «comprimido».

  2. Israel

    Otra cosa que se puede hacer es usar algo como un operador de eZ Publish de la extemsión eZJSCore. Lo que hace es que que en el momento de generar la página coge todos los js (y css) que cargas y los minifica y junta en un único archivo. Si por algún motivo se quiere ver los js originales, se puede hacer cambiando un parámetro.
    De esta manera no tienes que minificar los archivos «a mano» y si necesitas ver los originales lo puedes hacer fácilmente.

    • Carlos Benítez

      Estas herramientas suelen funcionar bien. Sin embargo, tienen un problema: hay veces que no empaquetan los archivos en el orden adecuado, sobre todo cuando hay módulos y dependencias cruzadas. En estos casos, meter todos los js en un solo archivo puede ocasionar errores en tiempo de ejecución.

      Por ejemplo, se me viene a la cabeza el caso de la plataforma e-commerce Magento: aquí, hay ocasiones en las que se pueden cargar hasta 20 archivos diferentes con referencias y objetos compartidos. En este escenario, el empaquetado era casi imprescindible pero siempre había problemas derivados precisamente de compactarlo todo. Al final, resultaba un lío el trazar las dependencias y ajustar el orden de los scripts para que se ejecutasen tal y como se esperaba.

      Saludos!

  3. Leandro

    Buenas, como siempre, interesante exposición sobre el asunto.
    Doy algunas opiniones basadas en mi experiencia personal:

    – Desde el punto del usuario común, estoy de acuerdo que un archivo comprimido (o no), no presenta mucha diferencia en tiempos de carga, salvo casos extraños (como dices, un archivo de 400 Kb..)
    – Desde el punto de vista del desarrollador, también pienso en la filosofia de no ofuscar el código, sino exponerlo para que otras personas puedan apreciarlo (ó contribuir a su mejora).
    – Sin embargo (aca viene la contraposición a los anteriores puntos) en muchos casos, la minificación y la reducción del tamaño de los archivos tiene como ventaja el menor consumo del ancho de banda del sitio. Y en sitios de alto trafico, esto es esencial (ya que más gasto de ancho de banda = mas dinero gastado).

    La pregunta entonces sería: ¿cómo trabajar utilizando la minificación de archivos, sin perfudicar a los desarrolladores y a los usuarios?. En mi caso, esto se soluciona trabajando con ambientes separados de integración continua. Por ejemplo: En al ambiente de desarrollo siempre se trabajan con las versiones normales de los archivos, y en los ambiente de producción se trabajan con las versiones comprimidas y unificadas. ¿Y que sucede con los usuarios que quieren ver nuestro código sin minificar?. Eso lo podemos solucionar, por ejemplo, teniendo un repositorio público en gitHub con todos los archivos fuente (y hasta es más util, ya que se tiene seguimiento de los cambios realizados o mejoras…)
    Igualmente, entiendo que esta solución no es la más «apta» para todo público.

    Otra cosa que quería comentar es que, por ejemplo, Closure Compiler de Google, posee varias opciones interesantes para comprimir el código, recomiendo mucho este artículo: http://bit.ly/e9H7sD

    Saludos

    • Carlos Benítez

      Tu punto de vista es perfectamente válido. Es cierto que ahorrar ancho de banda es importante, sin embargo, aquí también podríamos argumentar que hoy día este parámetro no es tan crítico como lo era hace algún tiempo (hablamos en términos económicos). Un servicio de hosting competente (entendemos aquí las soluciones compartidas) suelen ofrecer ancho de banda ilimitado (en España no es tan normal, pero fuera hay muchas alternativas de calidad).

      Pero al margen de ese detalle, la metodología que expones de mantener varias copias de los archivos separadas por entornos y colgar después los originales para un libre acceso me ha parecido simplemente fantástica. De hecho, nunca he trabajado para una empresa tan concienciada en la difusión del conocimiento: mi enhorabuena si es el caso de la tuya. Es un claro ejemplo a seguir.

      Gracias por la inspiración.
      Saludos!!

  4. Joel Gómez

    Muy buen articulo, aunque no soy especialmente promovedor del software libre entiendo y comparto la idea que expones en el post, creo que una de las razones por las que JavaScript se ha echo tan popular (aparte de ser necesario) es por la facilidad de compartir el codigo fuente, su accesibilidad por parte de terceros y posterior modificacion, creo que esa idea deberia de seguir intacta salvo contadas ocaciones, que como bien dices quedan a consideracion del desarrollador y/o equipo de desarrollo.

  5. JuniHH

    Bueno, en cierta oportunidad tuve una mala experiencia con un proyecto con el que use la minificación del .js.

    Prefiero escribir javascript puro y tenía por costumbre (si, en pasado) minificar mis «.js». Para eso siempre guardaba dos versiones, uno llamado «.min.js» y otro «.js» sin minificar. Cierto día tuve que hacer una corrección al .js y fué cuando me di cuenta que había cometido la torpeza de minificar ese archivo. Me tomó todo un largo día de trabajo volverlo a su condición «normal» y desde entonces cambié de parecer sobre ese método.

    Ahora lo que hago es que guardo el archivo como .php y le aplico una línea de PHP para comprimirlo con gzip, puesto que la minificación era la alternativa que usaba con los .js para hacerlos lo más livianos posible.

    Creo que se aprende con las malas lecciones. :-/

  6. fabian koliren

    Muy buen articulo, nunca utilice estas técnicas y el articulo detallado me permite comprender perfectamente los pros y contras de cada caso.
    Gracias

Deja un comentario

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