Minimizar documentos para optimizar la carga de los recursos

Herramienta web para minimizar código Controlar la rapidez con que se descargan las páginas de nuestro sitio web es una de las tareas de mantenimiento y supervisión rutinaria. Como usuarios sabemos que no hay nada más desesperante que una página lenta. Además buscadores como Google penalizan el posicionamiento por esta cuestión. Hace poco publiqué un tema sobre la minimización de documentos exponiendo que hay que optimizar la carga de los recursos. Pues ahora en este tema publico la herramienta para minimizar o minificar documentos con la que he llevado a cabo la minimización en este sitio. Hay cosas que no estarán directamente bajo nuestro control, como la velocidad de la red, la configuración del servidor si estamos en un alojamiento compartido o incluso el navegador que utilice el usuario. Pero lo que si podemos controlar son nuestras páginas. Y hay muchas cosas que podemos hacer. Una de ellas es minimizar o minificar los documentos HTML, CSS y JS.

En la captura de pantalla de la izquierda se muestra el resultado de minimizar la página índice de este sito. Partiendo de un tamaño de 43.6 KB se obtiene una reducción de casi un 21%. Pero con otros documentos como JS o CSS se obtienen reducciones superiores en torno al 50%. En la minimización de este sitio que llevé a cabo hace poco conseguí una reducción promedio de 19.4% sobre un total de 5700 KB de 185 documentos HTML, PHP, JS y CSS. Los PHP no se envían al navegador directamente, pero al ser de menor tamaño también supone que el servidor los manejará más rápidamente. En definitiva, la minimización es uno de los muchos pasos que hay que llevar a cabo para conseguir una web más rápida.

Descarga de la herramienta minimizador

Se ha de instalar primero el marco de herramientas y comprobar que funciona según lo esperado. Si es así puede ahora descargar el conjunto de carpetas tools-web.zip (40 KB) de este minimizador y descomprimirlas sobre la ya existente tools-web del marco de herramientas. Se debe permitir sobreescribir los archivos pues sólo se agregarán nuevos archivos o bien nuevas versiones de los existentes. Tras esto habremos agregado lo siguiente en la carpeta tools-web:

  • En la carpeta includes se agregan los siguientes módulos:
    • minimizador.php: Módulo para minimizar documentos.
    • xml-core.php: Un parseador de HTML imprescindible para minimizar los documentos.
    • xml.php: Extensiones al parseador anterior con algunas funciones equivalentes a las que gestionan el DOM.
  • En la carpeta piezas se agrega la carpeta minimizar que contiene los recursos de la herramienta que estamos tratando. Contiene estos archivos:
    • index.php: Interfaz HTML para gestionar la herramienta.
    • conf.txt: Configuración de la herramienta
    • minimizado.txt y ruta-clases-uri.txt: Archivos de texto, el primero para guardar el documento minimizado y el segundo es un archivo temporal para guardar datos para la herramienta.
  • En la carpeta res se agregan:
    • prueba.html: Documento para probar la herramienta
    • prueba.css: Documento para probar la herramienta

Los módulos xml-core.php y xml.php se incluyeron también en la descarga del resaltador de código. Debe permitir sobreescribirlos pues así siempre tendrá la última versión en el marco de herramientas. A no ser que haya realizado sus propios cambios en los PHP y quiera conservarlos.

IMPORTANTE:
  1. Esta aplicación está diseñada para ser usada única y exclusivamente en un servidor local, denominado a veces como localhost. No está preparada desde el punto de vista de la seguridad ni del uso de los recursos para que funcione en un servidor en producción.
  2. Realiza pruebas extensivas antes de usarla en contenidos definitivos. Haz copias de seguridad previas de los documentos si esta aplicación puede modificarlos.
  3. Ten en cuenta lo relativo a la licencia de uso.
  4. Si la utilizas te agradecería que me comunicaras tu impresión o incidencias en el formulario de contacto.

Funcionamiento del minimizador de documentos

Herramienta minimizador código

La minimización del código se consigue parseando el documento con el módulo xml-core.php. Es decir, se analiza el documento como si fuera un HTML, aún en el caso de un CSS, JS o PHP. En los casos de CSS y JS la herramienta engloba automáticamente todo el texto dentro de un elemento <style> o <script> respectivamente, aunque por supuesto sólo a efectos de poder analizar los nodos del documento pues esto no aparecerá en el resultado. Con PHP el texto ya tendrá que venir dentro de <?php ... ?>. Este módulo adicional xml-core.php analiza el documento extrayendo todos los elementos en un árbol de nodos. El nombre xml es sólo porqué inicialmente lo planteé para analizar documentos XHTML que son en última instancia XML.

Realmente lo único que hace el módulo xml-core.php es descargar los nodos en un árbol, analizando hasta cierto punto si están bien formados. Tampoco en ningún caso realiza validación alguna contra un DTD. Al usar esta herramienta podría encontrar errores de mal formación del HTML en el documento que vaya a minimizar. Pero no los encontraría todos. Por ejemplo, es necesario usar siempre etiqueta o tag de cierre, por lo que algo como <p>párrafo sin el tag final </p> sería para esta herramienta un error y no permitiría seguir ejecutándose el proceso de minimización. Sin embargo no verificaría el correcto anidamiento como con <span><i>ABC</span></i>. De todas formas un error como este es arreglado automáticamente por el minimizador, pero sería recomendable verificar la buena formación del HTML antes de proceder a minimizar un documento. E incluso también pasar la verificación al documento minimizado. De hecho es una buena práctica hacer esto antes de publicar algo, pues el código mal formado siempre es interpretado por el navegador pero puede ocasionar un resultado no previsto. Los editores de código o los IDE pueden servir para esto. Además de eso yo también utilizo otra herramienta propia que se apoya en el xml-core.php para verificar los documentos antes de publicarlos.

La acción de minimizar se inicia construyendo el árbol de nodos del documento detectando también los nodos texto. Hablo de nodos en lugar de elementos de un HTML porque un documento se compone de nodos, algunos de los cuales son elementos y otros son trozos de texto que se ubican dentro de los elementos. Esto quiere decir que un documento HTML que sólo contenga esto:

Realmente tiene la siguiente estructura para el analizador xml-core.php:

Los nodos texto se encierran temporalmente en un elemento configurable <TEXT_></TEXT_> para poder manejar el árbol. Esta técnica de modificar temporalmente el documento con el analizador xml-core.php es clave para procesarlo. Por ejemplo, un comentario como <!-- ABC --> se procesa como <COMMENT_> ABC </COMMENT_>. Otra muestra: los elementos vacíos como <img src="x" /> realmente se parsean de forma temporal como <img src="x" empty="empty"></img>, agregándole el tag de cierre y un atributo para reflejar su cualidad de elemento vacío. Estas modificaciones temporales se deshacen cuando recomponemos el documento. Siguiendo con el ejemplo, el árbol de nodos resultará con la siguiente estructura en un array:

Array ( [0] => Array ( [numero] => 0
                       [tag] => b
                       [padre] => -1
                       [nivel] => 0
                       [atributos] => Array ( [id] => xy )
                       [innerHTML] => <TEXT_>ABC</TEXT_> )
        [1] => Array ( [numero] => 1
                       [tag] => TEXT_
                       [padre] => 0
                       [nivel] => 1
                       [atributos] => Array ( )
                       [innerHTML] => ABC ) )

En ese HTML de ejemplo tenemos dos nodos, uno es el elemento <b>, el otro es el nodo de texto ABC. Minimizar el documento comprende entonces iterar por los nodos observando que el nivel es la profundidad del nodo. Así el nodo <b> en este ejemplo tiene un nivel 0 indicando que no tiene ningún padre, es decir, está en la raíz del documento (su padre es el nodo -1 indicador de la raíz). El nodo texto tiene un nivel 1 pues es hijo del nodo anterior. Como los nodos están en el árbol según su orden de aparición, cuando detectemos que la diferencia de niveles entre un nodo y el siguiente es mayor que -1 significa que habremos llegado a un nodo texto. Pongamos otro ejemplo para explicarlo. Supongamos que tenemos un documento con estos hipotéticos elementos:

Para simplificar el ejemplo omitimos espacios entre los elementos. De todas formas la estructura sería la siguiente pero teniendo en cuenta que los únicos nodos de texto que hay son "Texto" y "ABC":

<a>
    <b>
        <c>
            Texto
        </c>
    </b>
</a>
<d>
    ABC
</d>

La construcción del árbol de nodos se realiza según su orden de aparición en el documento, siendo i el número del nodo y ni su nivel de profundidad:

i Nodos ni ni-ni+1 ----------------------------------------------- 0 <a> 0 0-1 = -1 1 <b> 1 1-2 = -1 2 <c> 2 2-3 = -1 3 Texto 3 3-0 = 2 4 <d> 0 0-1 = -1 5 ABC 1 fin de array

Se observa que cuando se cierra un rama del árbol será cierto ni - ni+1 > -1, momento en el cual sabemos que hemos llegado al final de una rama. Si el elemento final es un nodo texto extraemos su innerHTML, el texto que estamos buscando para minimizar. Si el elemento final no tiene texto entonces innerHTML == "" y no hacemos nada. El texto se minimiza siguiendo este orden en el algoritmo:

  1. Si el padre del nodo texto es un comentario lo eliminamos. A excepción de los comentarios condicionales (algo como <!--[if gt ie 7]> ...) y los que escapan JavaScript (ya en desuso).
  2. En otro caso si el padre es un elemento <script>, el texto será el propio script. Lo minimizamos con una función específica para minimizar código JavaScript.
  3. En otro caso si el padre es un elemento <style>, el texto también será el contenido CSS que se minimiza con otra función específica para ese cometido.
  4. En otro caso se resume uno o más espacios de cualquier clase en un único espacio simple, lo cual es una regla básica HTML para renderizar los documentos. También se eliminan los espacios entre elementos de bloque. Todo esto a excepción de que el padre sea un <pre>, <textarea> así como cualquier otro declarado en la configuración, como los que tengan estilo white-space, por ejemplo. En estos casos el texto se deja tal cual, respetando los posibles saltos de línea, tabulaciones, etc.

Todo los nodos se van apilando con array_push($pila, $nodo["tag"]) al ir analizando el array del árbol de nodos. Vamos agregando el nombre de la etiqueta o tag del nodo en una pila. Cuando llegamos al final de una rama desapilamos con array_pop($pila), es decir, obtenemos los tags en orden inverso y vamos cerrando elementos incorporándoles el texto minimizado. Esto es a modo de resumen el funcionamiento básico de la minimización que se apoya en el árbol de nodos construido con el módulo xml-core.php.

Una pila es una estructura de datos que se van acumulando en el orden en que llegan y se extraen en el orden inverso. Así el último en llegar es el primero en salir, por lo que se dice que su modo de acceso es LIFO: Last In First Out. Es lo contrario de una cola que tiene el modo FIFO, First In First Out, primero que llega, primero que sale.

La minimización de JavaScript: El problema del punto y coma

Para minimizar código JavaScript es imprescindible que las sentencias estén separadas por punto y coma. Sabemos que se pueden separar con un salto de línea. De hecho inicialmente escribía JavaScript así. Esto era una mal hábito adquirido con Visual Basic. Pero no veo la forma de minimizar JavaScript si las sentencias están delimitadas por un salto de línea. Además la función minimizar_js_php() dentro del módulo minimizador.php se encarga también de minimizar PHP. Y para éste si que es obligatorio el punto y coma. Así que para poder tener ambas cosas integradas no tuve más remedio que revisar los JavaScript más antiguos de este sitio para incorporarle los signos de punto y coma. Por supuesto, un IDE me ayudó para encontrarlos más fácilmente.

Otro detalle a tener en cuenta es la minimización de patrones de expresiones regulares en JavaScript. Sabemos que en PHP puede ser algo como "/.*/i" encerrado en comillas, pero en JavaScript se declaran sin comillas /.*/i. La minimización de script se basa en extraer temporalmente las cadenas entrecomilladas antes de quitar espacios. Pero extraer cadenas entre barras es más complejo. Hay un campo en la configuración de la herramienta que declara los métodos de JavaScript que pueden contener un patrón de expresión regular como argumento. He puesto replace,exec,match,search,test aunque debe revisar si se me quedó alguna atrás que desconozca. En definitiva,hay que revisar y comprobar que los script siguen funcionando según lo esperado tras la minimización.

Opciones de configuración del minimizador de documentos

Como las otras piezas del marco de herramientas, se agrupan las opciones de la interfaz en pestañas. En la primera se dispone de la posibilidad de minimizar un documento especificando una ruta o bien un trozo de texto. El resultado minimizado se puede guardar en un archivo y en todo caso también se presenta en la pestaña de resultados, donde se ofrece también un resumen de la ejecución (ver la primera imagen de este tema). En la pestaña de opciones podemos declarar algunas configuraciones para minimizar el código:

  • Lista de estilos CSS de los elementos cuyo contenido no se minimiza como white-space: pre.
  • Lista de etiquetas HTML cuyo contenido no se minimiza como <pre> y <textarea>.
  • Lista de atributos HTML de elementos cuyo contenido no se minimiza. Por ejemplo, podemos poner un atributo data-nomin a los elementos que queremos que no se minimize su contenido.
  • Lista de clases CSS de elementos (atributo class) cuyo contenido no se minimiza. Por ejemplo, podemos poner una clase-nomin a los elementos que queremos que no se minimize su contenido.
  • Lista de etiquetas HTML donde se pueden suprimir el espacio entre ellos. Son los elementos de bloque como html, head, title, meta, link, ...
  • Lista de operadores JavaScript y PHP.
  • Lista de métodos JavaScript con expresiones regulares, tales que puedan contener una expresión regular en entre barras, p.e.: xxx.replace(/abc/i, ...).

Las exclusiones de minimización relacionadas con estilo CSS son tratadas sobre el estilo en línea (el que se declara en el atributo style de un elemento), el estilo interno de los elementos <style> que pueda haber en la cabecera del documento y el estilo externo de los archivos vinculados con <link rel="stylesheet"...>. De hecho se construye un array de clases para que esté disponible en la herramienta y consultar el estilo al encontrar un atributo class en algún elemento. Este array se guarda al finalizar en un archivo rutas-clases-uri.txt para una finalidad diferente que la que necesita esta herramienta y que está relacionado con un publicador de documentos que ya estoy usando para minimizar múltiples documentos, entre otros objetivos. Pero esto requiere de un módulo paginador que, quizás, publique en un futuro.