Wextensible

Comprimir web con Apache mod_deflate

Cabecera de compresión GZIP

Uno de los mayores problemas que nos encontramos para disminuir los tiempos de carga de las páginas web tiene que ver con el tamaño de los recursos. Puesto que estos datos deben viajar por la red, cuanto menor sea su volumen tanto más rápido los tendremos disponibles en el navegador. Una forma de reducir ese volumen es comprimiendo los recursos antes de ser enviados por el servidor.

Los servidores Apache disponen de técnicas para comprimir recursos antes de ser enviados. El módulo mod_deflate nos serviría para esto. En primer lugar hemos de comprobar que el módulo está cargado, lo que podemos ver en el archivo de configuración httpd.conf del servidor Apache. Si estamos en un alojamiento compartido tendremos que consultarlo al administrador del sistema:

    
LoadModule deflate_module modules/mod_deflate.so    
    

Una forma de comprimir documentos es usando la directiva AddOutputFilter para lo cual debe estar cargado el módulo mod_mime que asocia las extensiones de los documentos con ciertos comportamientos y tipos de contenidos.

    
LoadModule mime_module modules/mod_mime.so  
    

Suele estar cargado por defecto, pues maneja además otras directivas importantes como AddDefaultCharset para que el servidor envíe una cabecera de codificación del documento. Con estos dos módulos podemos incorporar el siguiente código en el httpd.conf o en el archivo .htaccess si no tenemos acceso a aquella configuración.

    
AddOutputFilter DEFLATE .js .css .html .php
    

Podemos agregar todas las extensiones de documentos que manejemos, pero no se debe incluir las de imágenes como png, jpg o gif por ejemplo, pues estos tipos de archivos ya están de por sí comprimidos. Una ulterior compresión puede incluso conducir a incrementar su tamaño.

Si utilizamos htaccess incluíriamos el código anterior en ese archivo ubicado en la carpeta raíz. Si no queremos que se compriman recursos en alguna de sus subcarpetas, podemos eliminar la compresión ubicando un htaccess en esa carpeta:

    
RemoveOutputFilter .html .js .css .php
    

Todo esto funciona perfectamente en mi servidor Apache 2.2.15 montado en localhost, incluyendo esa directiva AddOutputFilter DEFLATE en el htaccess de la carpeta raíz. Una captura de pantalla de Developers Tools de Chrome nos permite observar las cabeceras de una petición:

Cabeceras request y response

Se trata del archivo general.js que ocupa 42 KB ya minimizado (o minificado) y que vinculo en todas las páginas principales del sitio. Tras comprimir queda en unos 12 KB, lo que supone un 71% de reducción. Es decir, nos estamos ahorrando unos 30 KB en cada petición de este archivo. Veáse en la imagen anterior como el navegador envía una cabecera Accept-Encoding informando al servidor de los tipos de compresión que soporta. El servidor le envía el archivo comprimido y, previamente, dos cabeceras de respuesta. La primera Content-Encoding: gzip para notificar con el modo de compresión y otra Vary: Accept-Encoding para alertar a los proxies que envíen documentos comprimidos en caché sólo a navegadores que soporten la descompresión.

Comprimir web con PHP instalado como módulo. Scripts PHP de pre-ejecución

Puede suceder que estemos en un alojamiento compartido y por alguna razón el servidor no nos permitan comprimir la salida de archivos con las directivas de Apache. Entonces podemos usar PHP para ese cometido. Primero hemos de tener en cuenta como se ejecuta PHP en el servidor Apache. En mi servidor de pruebas en localhost lo tengo montado como módulo. Cuando instalé PHP después de instalar Apache, la instalación agregó las siguientes líneas al final del fichero de configuración httpd.conf de Apache:


#BEGIN PHP INSTALLER EDITS - REMOVE ONLY ON UNINSTALL
PHPIniDir "C:/php/"
LoadModule php5_module "C:/php/php5apache2_2.dll"
#END PHP INSTALLER EDITS - REMOVE ONLY ON UNINSTALL
    

Esto quiere decir que PHP se carga como un módulo más de los que maneja el servidor. Entonces podemos realizar ciertos ajustes de configuración PHP desde los archivos httpd.conf o .htaccess. Las opciones pueden ser php_flag name on|off o php_value name value, siendo la primera para configuraciones que admitan valores booleanos (on|off) y la segunda para resto de valores. Podemos usar la compresión PHP ZLIB agregando estas directivas a nuestro .htaccess:

    
php_flag zlib.output_compression  On
php_value zlib.output_compression_level 9
    

Esto lo pondríamos en el .htaccess de la carpeta raíz y todos los documentos en esta y restantes subcarpetas tratados por el servidor como PHP serán comprimidos con nivel máximo por el módulo de PHP. ¿Y cómo sabe Apache que tipos de archivos debe enviar al módulo PHP para que éste los maneje?. Otro archivo de configuración de Apache es mime.types que contiene una larga lista relacionando el tipo MIME con la extensión del archivo. La instalación de PHP tras la de Apache agrega una línea como la siguiente al final de ese archivo:

    
application/x-httpd-php   php
    

Así todos los archivos con extensión php serán tratados como documentos PHP y por tanto se les aplicará la configuración de compresión antes descrita. Podemos tratar los archivos con extensiones html, js y css para que sean también manejados por PHP y por tanto sean también comprimidos. En un alojamiento compartido no podemos modificar el archivo mime.types, pero si podemos agregar una directiva AddType al htaccess de la carpeta raíz del sitio:

    
AddType application/x-httpd-php .html .js .css
    

Estos documentos manejados con mime type php son enviados al navegador con una cabecera Content-Type: text/html pues esto queda determinado en el archivo de configuración php.ini de mi localhost:

    
; PHP's built-in default is text/html
; http://php.net/default-mimetype
default_mimetype = "text/html"
    

Para un archivo html esto es correcto, pero no así para los js o css. Entonces necesitamos dar otro paso más para modificar la cabecera Content-Type de esos archivos. Si consulto otra vez el php.ini de mi localhost observo esto:

    
; Automatically add files before PHP document.
; http://php.net/auto-prepend-file
;auto_prepend_file =

; Automatically add files after PHP document.
; http://php.net/auto-append-file
;auto_append_file =
    

Aparecen inicialmente comentadas las opciones de configuración auto_prepend_file y auto_append_file. Con ellas podemos agregar un archivo PHP de pre-ejecución para que se ejecute antes que cualquier salida PHP. Esto sería adecuado para modificar ahí los Content-Type de los archivos js y css. Si no queremos o no podemos tocar ese archivo de configuración podemos hacerlo en un .htaccess y ponerlo junto a los que ya indicamos más arriba para comprimir:

    
php_flag zlib.output_compression  On
php_value zlib.output_compression_level 9
php_value auto_prepend_file RUTA 
    

Hemos de incluir en RUTA la ruta física donde se encontrará ese script PHP de pre-ejecución. En mi localhost sobre Windows ese archivo lo denomino prehjc.php y la ruta viene a ser algo como "C:\???\Apache server\htdocs\???\prehjc.php", donde he acortado carpetas intermedias con los tres signos de interrogación por simplicidad. Es mejor encerrar todo en comillas por el tema de los espacios en la ruta.

Si no queremos estar escribiendo esa ruta física que puede ser muy larga, o incluso para el caso de que estemos en un alojamiento compartido, podemos preparar un pequeño archivo de texto que contenga sólo esto <?php echo getcwd(); ?> y subirlo con extensión php a la carpeta donde fueramos a poner nuestro archivo de pre-ejecución. Luego en la barra de direcciones de un navegador pondríamos la ruta relativa a la carpeta raíz y lo ejecutaríamos, dándonos la ruta física. Si la carpeta estuviera por fuera de la raíz no podríamos llamarlo directamente, por lo que tendríamos que crear dos pequeños archivos. Uno sólo con <?php $ruta = getcwd(); ?> que se ubicaría en la carpeta por fuera de la raíz. Luego llamaríamos a ese PHP desde otro que estuviera por ejemplo en la misma carpeta raíz con este contenido:

<?php
include($_SERVER["DOCUMENT_ROOT"]."/../???/ruta.php");
echo $ruta;
?>

Este PHP sería el que ejecutaríamos desde la barra de direcciones de un navegador. Con include ejecutamos el otro PHP en esa carpeta externa a la raiz y que he obviado poniendo los tres signos de interrogación pues dependerá de cada caso.

El archivo prehjc.php que se pre-ejecuta puede contener algo básico como esto:

<?php
    $url = $_SERVER["PHP_SELF"];
    $ruta = pathinfo($url);
    $extension = $ruta["extension"];
    if ($extension == "html"){
        header("Content-Type: text/html; charset=utf-8");
    } else if($extension == "css") {
        header("Content-type: text/css");
    } else if($extension == "js") {
        header("Content-type: text/javascript");
    }
?>

Si la extensión es html no haría falta enviar la cabecera con header, pero aprovechamos también para enviar una codificación de caracteres. Para los otros dos tipos de archivos se enviarán los Content-Type adecuados. Tras hacer todos estos cambios en mi localhost y solicitando una página cualquiera del sitio obtengo esta cabecera de respuesta en la herramienta Firebug de Firefox para el documento general.js:

Cabecera response recurso comprimido

Se observa que comprime hasta los 12,1 KB (recuerde que ocupa 42 KB sin comprimir) así como las cabeceras adecuadas del Content-Type, Content-Encoding y Vary.

Comprimir web con PHP (instalado como CGI)

Istalado como módulo:
Php instalado como módulo
Instalado como CGI:
Php instalado como CGI

Si nuestro sitio está en un alojamiento compartido, es probable que PHP esté instalado como CGI. Para saberlo podemos subir un archivo PHP con el contenido <?php echo phpinfo(); ?> que nos dará un largo informe de todas las opciones de configuración. Al inicio de dicho informe podemos obtener algo como lo de las imágenes adjuntas. En la primera se observa que Server API es "Apache 2.0 Handler" y por tanto PHP está instalado como módulo. Esa es una captura de pantalla del phpinfo de mi localhost. La segunda imagen es la del servidor actual donde está viendo esta página. En este caso PHP se maneja con "CGI/FastCGI", con lo cual se ha de tener en cuenta algunas consideraciones para lograr comprimir el contenido.

Con CGI hemos de cambiar el tipo para servir como php las extensiones html, js y css. Pero la forma exacta de como hay que hacerlo puede que difiera en cada servidor. Por ejemplo en 1and1 que es donde yo tengo este sitio, usan un manejador cgi-script denominado x-mapp-php5 que agregaremos usando la directiva AddHandler en un htaccess. Pero al mismo tiempo hay que añadir un nuevo tipo x-mapp-php5 para que ese servidor use la versión 5 de PHP. En definitiva, hay que poner en el htaccess de la carpeta raíz las siguientes directivas:

    
AddType x-mapp-php5 .html .js .css
AddHandler x-mapp-php5 .html .js .css
    

Este contenido estaría en el htaccess de la carpeta raíz y se propagaría a todas las carpetas hijas. Si en alguna no quisiéramos que se activara debemos remover el manejador y volver a redeclarar los tipos html, css y js. Pondríamos las siguientes directivas en un htaccess en esa subcarpeta:

    
RemoveHandler .html .js .css
AddType text/html .html
AddType text/css .css
AddType text/javascript .js
    

Con esto los documentos html, css y js ya no serán tratados como documentos php. En este sitio lo aplico a los documentos dentro de carpetas con nombre ejemplos, pues esos documentos suelen ser de muy pequeño tamaño y además algunos son de ejemplos de ejecución PHP y no quiero que interfieran con el archivo PHP de pre-ejecución.

Por otra lado con CGI no podemos usar los ajustes de configuración php_flag o php_value en un archivo htaccess. Pero si es posible que podamos usar archivos php.ini de usuario en cada carpeta donde queramos comprimir sus documentos. Cada uno de estos archivos contendría lo siguiente:

    
zlib.output_compression = On
zlib.output_compression_level = 9
auto_prepend_file = "Ruta PHP pre-ejecución"
    

Es un poco de engorro tener que poner estos archivos en todas las carpetas, pero al mismo tiempo la ventaja es que en aquellas donde no lo pongamos no se aplicará la compresión. Por lo tanto en mis carpetas de ejemplos (y hay muchas) no tendré que incluirlos.

Combinar ajustes en desarrollo y producción

Podemos liarnos si estamos desarrollando nuestro sitio en localhost con PHP instalado como módulo y luego subimos esos documentos al sitio en producción con PHP instalado como CGI. En cuanto a los archivos php.ini en cada carpeta, estos no se ven afectados en localhost con PHP como módulo, pues simplemente los ignorará. Sólo tendremos que recordar que cuando creemos una nueva carpeta en localhost debemos agregar una copia de ese archivo. Pero con el htaccess de la carpeta raíz y también aquellos de las subcarpetas que anulan la compresión deberíamos tener dos versiones, una para localhost y otra para el sitio en producción.

Para evitar esto podemos unificar ambas acciones en el htaccess de la carpeta raíz con el siguiente código:

#Comprimir

#php como módulo en localhost
<IfModule mod_php5.c>
    #AddOutputFilter DEFLATE .js .css .html .php
    php_flag zlib.output_compression  On
    php_value zlib.output_compression_level 9
    AddType application/x-httpd-php .html .js .css
    php_value auto_prepend_file "Ruta en localhost al PHP de pre-ejecución"
</IfModule>

#php como cgi (en 1and1). La compresión se realiza en
#los php.ini de las carpetas donde se quiera comprimir
<IfModule !mod_php5.c>
    AddType x-mapp-php5 .html .js .css
    AddHandler x-mapp-php5 .html .js .css
</IfModule>

#Ocultar php.ini
<Files "php.ini">
    Order allow,deny
    Deny from all
    Satisfy All
</Files>

Podemos detectar si PHP está cargado como módulo preguntando por IfModule mod_php5.c. En ese caso estaremos en nuestro localhost y podemos comprimir con Apache usando AddOutputFilter DEFLATE o usando la compresión con PHP. En caso de que no esté cargado como módulo agregaremos el manejador y el tipo x-mapp-php5 para 1and1, que conjuntamente con los archivos php.ini en cada carpeta realizará la compresión con PHP. Como medida de seguridad adicional ocultaremos esos archivos php.ini para que no puedan ser llamados desde un navegador.

En las carpetas donde no queramos ejecutar la compresión pondríamos este htaccess:

<IfModule mod_php5.c>
    #RemoveOutputFilter .html .js .css
    AddType text/html .html
    AddType text/css .css
    AddType text/javascript .js    
</IfModule>
<IfModule !mod_php5.c>
    RemoveHandler .html .js .css
    AddType text/html .html
    AddType text/css .css
    AddType text/javascript .js
</IfModule>

Si en localhost hubiésemos usado AddOutputFilter ahora lo quitaríamos. En otro caso volveríamos a reponer los tipos html, css y js. En el servidor CGI tendríamos además que quitar el manejador. Con todo esto tendremos unificado ambos comportamientos y no tendremos que estar modificando o teniendo dos versiones de estos htaccess para cuando vayamos a subir documentos a nuestro sitio en producción.