Lo hice y lo entendí

El blog de Vicente Navarro
12 abr

Cómo servir contenidos comprimidos de forma estática con Apache

Hace unos meses escribí sobre cómo comprimir los contenidos servidos por Apache para no consumir tanto ancho de banda y además, sobre cómo hacerlo cacheando la compresión para que Apache no tenga que comprimir una y otra vez los mismos contenidos consumiendo CPU:

Así, yo venía usando WP Super Cache para comprimir y cachear WordPress y mod_deflate+mod_cache para los ficheros CSS y los JS.

Sin embargo, cuando en Febrero pasé a hospedar el blog en 1and1.es, me encontré con que usan Apache 1.3 (no 2.x) y que el módulo equivalente al mod_deflate de Apache 2.x, el mod_gzip no está disponible.

Cuando el hosting era casero, la preocupación por el ancho de banda era mayormente por los usuarios, para que no tuvieran que esperar demasiado a que los contenidos se cargaran y ahora, con 1and1.es, la preocupación es por no exceder la cuota de transferencia mensual. Por supuesto, con compresión los usuarios siempre salen ganando, ya que con la potencia de las máquinas actuales, la carga de tener que estar descomprimiendo los contenidos descargados es mínima.

Por tanto, ahora el nuevo objetivo es servir los CSS y los JS comprimidos sin tener ni mod_deflate ni mod_gzip a nuestra disposición.

Como muestra de que esto se podía hacer, yo me había fijado en que WP Super Cache lo que hacía era crear páginas estáticas .html y .html.gz en wp-content/cache/supercache, añadir el siguiente .htaccess al directorio wp-content/cache:

# BEGIN supercache
AddEncoding x-gzip .gz
AddType text/html .gz
# END supercache

y poner reglas como estas en el .htaccess del directorio raíz del blog:

RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{DOCUMENT_ROOT}/wp-content/cache/supercache/%{HTTP_HOST}/$1index.html.gz -f
RewriteRule ^(.*) /blog/wp/wp-content/cache/supercache/%{HTTP_HOST}/$1index.html.gz [L]
 
RewriteCond %{DOCUMENT_ROOT}/wp-content/cache/supercache/%{HTTP_HOST}/$1index.html -f
RewriteRule ^(.*) /wp-content/cache/supercache/%{HTTP_HOST}/$1index.html [L]

La RewriteCond:

RewriteCond %{HTTP:Accept-Encoding} gzip

evalúa las cabeceras de la petición HTTP para ver si el cliente acepta contenido comprimido con gzip y, en tal caso, sirve el fichero comprimido, el index.html.gz. En caso contrario, sirve el normal, el index.html.

Pero la clave de todo esto es la directiva AddEncoding:

AddEncoding x-gzip .gz

Si instalamos un Apache en Debian, su configuración por defecto en lo que respecta a ficheros .gz es la siguiente (/etc/apache2/apache2.conf):

    # AddEncoding allows you to have certain browsers uncompress                                    
    # information on the fly. Note: Not all browsers support this.                                  
    # Despite the name similarity, the following Add* directives have                               
    # nothing to do with the FancyIndexing customization directives above.                          
    #                                                                                               
    #AddEncoding x-compress .Z                                                                      
    #AddEncoding x-gzip .gz .tgz                                                                    
    #                                                                                               
    # If the AddEncoding directives above are commented-out, then you                               
    # probably should define those extensions to indicate media types:                              
    #                                                                                               
    AddType application/x-compress .Z                                                               
    AddType application/x-gzip .gz .tgz 

Actualización 6/2/09: En Debian Lenny estas líneas están en /etc/apache2/mods-available/mime.conf, no en /etc/apache2/apache2.conf.

Con ella, lo que hacemos es decirle a Apache que los ficheros .gz son ficheros con el tipo MIME application/x-gzip (List of IANA registered MIME Media Types). Si abrimos un fichero .gz con esta configuración, el resultado será que el navegador nos pregunta con qué aplicación queremos abrir el fichero o si simplemente queremos guardarlo:

application/x-gzip

Y accediendo al fichero con wget y la opción -S para ver las cabeceras comprobamos que, efectivamente, el Content-Type es application/x-gzip:

$ wget -S http://localhost/prueba.html.gz
--13:02:36--  http://localhost/prueba.html.gz
           => `prueba.html.gz'
Resolving localhost... 127.0.0.1
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 
  HTTP/1.1 200 OK
  Date: Sat, 12 Apr 2008 11:02:36 GMT
  Server: Apache/2.2.3 (Debian)
  Last-Modified: Sat, 12 Apr 2008 10:46:03 GMT
  ETag: "1bb50e-bb61-c1ca50c0"
  Accept-Ranges: bytes
  Content-Length: 47969
  Keep-Alive: timeout=15, max=100
  Connection: Keep-Alive
  Content-Type: application/x-gzip
Length: 47,969 (47K) [application/x-gzip]
 
100%[====================================>] 47,969        --.--K/s             
 
13:02:36 (31.35 MB/s) - `prueba.html.gz' saved [47969/47969]

Nuestro objetivo es, por tanto, que ese fichero .gz se sirva como text/html. Para ello, lo primero es comentar las siguientes líneas del fichero apache2.conf (y reiniciar Apache con "apache2ctl graceful"):

    #AddType application/x-compress .Z                                          
    #AddType application/x-gzip .gz .tgz 

y a continuación añadir en el .htaccess del directorio que contiene los ficheros que queramos servir comprimidos la línea del AddEncoding:

AddEncoding x-gzip .gz

Ahora, si volvemos a acceder al fichero, el Content-Type es text/html, con "Content-Encoding: x-gzip", así que esta vez sí que hemos servido un documento HTML comprimido de forma estática y además, los navegadores interpretan el documento correctamente:

$ wget -S http://localhost/prueba.html.gz                 
--13:19:06--  http://localhost/prueba.html.gz
           => `prueba.html.gz'
Resolving localhost... 127.0.0.1
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 
  HTTP/1.1 200 OK
  Date: Sat, 12 Apr 2008 11:19:06 GMT
  Server: Apache/2.2.3 (Debian)
  Last-Modified: Sat, 12 Apr 2008 10:46:03 GMT
  ETag: "1bb50e-bb61-c1ca50c0"
  Accept-Ranges: bytes
  Content-Length: 47969
  Keep-Alive: timeout=15, max=100
  Connection: Keep-Alive
  Content-Type: text/html; charset=UTF-8
  Content-Encoding: x-gzip
Length: 47,969 (47K) [text/html]
 
100%[====================================>] 47,969        --.--K/s             
 
13:19:06 (38.31 MB/s) - `prueba.html.gz' saved [47969/47969]

Lo mismo ocurre si a lo que accedemos es a un fichero CSS (text/css) o a un fichero JS (application/x-javascript):

$ wget -S http://localhost/style.css.gz                           
--13:22:56--  http://localhost/style.css.gz
           => `style.css.gz'
Resolving localhost... 127.0.0.1
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 
  HTTP/1.1 200 OK
  Date: Sat, 12 Apr 2008 11:22:56 GMT
  Server: Apache/2.2.3 (Debian)
  Last-Modified: Sat, 12 Apr 2008 11:22:33 GMT
  ETag: "1bb861-a42-44531040"
  Accept-Ranges: bytes
  Content-Length: 2626
  Keep-Alive: timeout=15, max=100
  Connection: Keep-Alive
  Content-Type: text/css
  Content-Encoding: x-gzip
Length: 2,626 (2.6K) [text/css]
 
100%[====================================>] 2,626         --.--K/s             
 
13:22:56 (23.36 MB/s) - `style.css.gz' saved [2626/2626]

Bueno, ya estamos sirviendo contenidos comprimidos de forma estática pero, ¿cómo hacerlo para no tener que usar las extensiones .html.gz, .css.gz o .js.gz? Pues lo único que tenemos que hacer es tener la opción de Apache Multiviews habilitada, tal y como explica la documentación de Apache: Content Negotiation.

La opción MultiViews lo que hace es permitir que Apache seleccione automáticamente la extensión del fichero en función de los ficheros existentes en el directorio. Por ejemplo, si pedimos el fichero prueba.html y el prueba.html no existe, el Apache nos devolverá el prueba.html.gz de forma transparente y, además, si nos fijamos en las cabeceras veremos el "Content-Location: prueba.html.gz":

# wget -S http://localhost/prueba.html
--13:58:53--  http://localhost/prueba.html
           => `prueba.html'
Resolving localhost... 127.0.0.1
Connecting to localhost|127.0.0.1|:80... connected.
HTTP request sent, awaiting response... 
  HTTP/1.1 200 OK
  Date: Sat, 12 Apr 2008 11:58:53 GMT
  Server: Apache/2.2.3 (Debian)
  Content-Location: prueba.html.gz
  Vary: negotiate
  TCN: choice
  Last-Modified: Sat, 12 Apr 2008 10:46:03 GMT
  ETag: "1bb50e-bb61-c1ca50c0;5f905480"
  Accept-Ranges: bytes
  Content-Length: 47969
  Keep-Alive: timeout=15, max=100
  Connection: Keep-Alive
  Content-Type: text/html; charset=UTF-8
  Content-Encoding: x-gzip
Length: 47,969 (47K) [text/html]
 
100%[====================================>] 47,969        --.--K/s             
 
13:58:53 (42.22 MB/s) - `prueba.html' saved [47969/47969]

Las MultiViews también sirven, por ejemplo, para poder pedir "http://localhost/prueba", así sin extensión, y que se sirva el fichero prueba.html o el prueba.html.gz según el cliente acepte compresión o no.

Aunque hay que tener cuidado con las MultiViews porque puede tener sus riesgos, especialmente con Google y ficheros PHP : Beware of Apache’s Multiviews.

Y ya casi lo tenemos, sólo con que tengamos el MultiViews, que comprimamos los ficheros que queramos servir comprimidos con gzip y que pongamos el AddEncoding, ya estaremos sirviendo contenidos comprimidos estáticamente. Sin embargo, aún tenemos un problema, y es que tenemos que respetar a los clientes HTTP que no acepten contenido comprimido. Por eso, no podemos simplemente comprimir ese fichero style.css y dejar un style.css.gz, ya que lo adecuado es que tengamos un style.css y un style.css.gz en el mismo directorio.

Para atajar esto, podemos seguir la misma táctica que seguía el WP Super Cache con las RewriteCond y RewriteRule y así, yo, por ejemplo, ahora en mi nuevo hosting tengo las siguientes líneas en el .htaccess del directorio de la plantilla de WordPress para servir los ficheros CSS comprimidos:

AddEncoding x-gzip .gz
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteRule ^style.css$ /blog/wp/wp-content/themes/lhyle08/style.css.gz [L]
RewriteRule ^print.css$ /blog/wp/wp-content/themes/lhyle08/print.css.gz [L]
</IfModule>

Es decir, si el cliente acepta compresión, el Apache reescribe la URL para servir el el style.css.gz y el print.css.gz y si no, pues los ficheros CSS normales.

Si nos fijamos, en los wget de los ejemplos anteriores, el contenido era servido con "Content-Encoding: x-gzip" aunque el cliente no aceptara dicha compresión, que es lo que hace wget por defecto. Ahora ya vamos a respetarlo y así, si pedimos el fichero CSS sin aceptar compresión nos llega el fichero normal:

$ wget -S http://www.vicente-navarro.com/blog/wp/wp-content/themes/lhyle08/style.css
--14:16:51--  http://www.vicente-navarro.com/blog/wp/wp-content/themes/lhyle08/style.css
           => `style.css'
Resolving www.vicente-navarro.com... 87.106.205.39
Connecting to www.vicente-navarro.com|87.106.205.39|:80... connected.
HTTP request sent, awaiting response... 
  HTTP/1.1 200 OK
  Date: Sat, 12 Apr 2008 12:16:51 GMT
  Server: Apache/1.3.34 Ben-SSL/1.55
  Last-Modified: Sat, 15 Mar 2008 21:15:56 GMT
  ETag: "181f4-2a0e-47dc3c8c"
  Accept-Ranges: bytes
  Content-Length: 10766
  Keep-Alive: timeout=2, max=200
  Connection: Keep-Alive
  Content-Type: text/css
Length: 10,766 (11K) [text/css]
 
100%[====================================>] 10,766        55.61K/s             
 
14:16:51 (55.51 KB/s) - `style.css' saved [10766/10766]

y si aceptamos la compresión, nos llega comprimido, con lo que hemos logrado nuestro objetivo:

$ wget -S --header="Accept-Encoding: gzip" http://www.vicente-navarro.com/blog/wp/wp-content/themes/lhyle08/style.css
--14:18:25--  http://www.vicente-navarro.com/blog/wp/wp-content/themes/lhyle08/style.css
           => `style.css'
Resolving www.vicente-navarro.com... 87.106.205.39
Connecting to www.vicente-navarro.com|87.106.205.39|:80... connected.
HTTP request sent, awaiting response... 
  HTTP/1.1 200 OK
  Date: Sat, 12 Apr 2008 12:18:25 GMT
  Server: Apache/1.3.34 Ben-SSL/1.55
  Last-Modified: Tue, 01 Apr 2008 10:03:40 GMT
  ETag: "1d366-9f2-47f2087c"
  Accept-Ranges: bytes
  Content-Length: 2546
  Keep-Alive: timeout=2, max=200
  Connection: Keep-Alive
  Content-Type: text/css
  Content-Encoding: gzip
Length: 2,546 (2.5K) [text/css]
 
100%[==============================================================================================>] 2,546         --.--K/s             
 
14:18:25 (665.05 KB/s) - `style.css' saved [2546/2546]

La RewriteRule que yo he usado es muy específica, puesto que sólo servía para dos ficheros muy concretos de mi página. Si se quisiera hacer de forma más genérica para servir multitud de ficheros .gz diferentes, podríamos hacerlo de la siguiente forma:

AddEncoding x-gzip .gz
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_FILENAME}.gz -f
RewriteRule ^(.+).(html|css|js)$ $1.$2.gz [L]
</IfModule>

Introducimos una nueva RewriteCond para evaluar si el fichero solicitado, de extensión .html, .css o .js, existe de forma comprimida y, en tal caso, servirlo.

Si la última RewriteRule fuera aún más genérica, sin especificar ninguna extensión en concreto:

RewriteRule ^(.+)$ $1.gz [L]

también funcionaría, pero si activamos el RewriteLog, veremos que la regla se activa dos veces, una para prueba.html.gz y otra para prueba.html.gz.gz:

127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8210820/initial] (3) [perdir /var/www/apache2-default/] strip per-dir prefix: /var/www/apache2-default/prueba.html -> prueba.html
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8210820/initial] (3) [perdir /var/www/apache2-default/] applying pattern '^(.+)$' to uri 'prueba.html'
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8210820/initial] (4) [perdir /var/www/apache2-default/] RewriteCond: input='gzip' pattern='gzip' => matched
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8210820/initial] (4) [perdir /var/www/apache2-default/] RewriteCond: input='/var/www/apache2-default/prueba.html.gz' pattern='-f' => matched
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8210820/initial] (2) [perdir /var/www/apache2-default/] rewrite 'prueba.html' -> 'prueba.html.gz'
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8210820/initial] (3) [perdir /var/www/apache2-default/] add per-dir prefix: prueba.html.gz -> /var/www/apache2-default/prueba.html.gz
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8210820/initial] (2) [perdir /var/www/apache2-default/] strip document_root prefix: /var/www/apache2-default/prueba.html.gz -> /prueba.html.gz
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8210820/initial] (1) [perdir /var/www/apache2-default/] internal redirect with /prueba.html.gz [INTERNAL REDIRECT]
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8219ab8/initial/redir#1] (3) [perdir /var/www/apache2-default/] strip per-dir prefix: /var/www/apache2-default/prueba.html.gz -> prueba.html.gz
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8219ab8/initial/redir#1] (3) [perdir /var/www/apache2-default/] applying pattern '^(.+)$' to uri 'prueba.html.gz'
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8219ab8/initial/redir#1] (4) [perdir /var/www/apache2-default/] RewriteCond: input='gzip' pattern='gzip' => matched
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8219ab8/initial/redir#1] (4) [perdir /var/www/apache2-default/] RewriteCond: input='/var/www/apache2-default/prueba.html.gz.gz' pattern='-f' => not-matched
127.0.0.1 - - [12/Apr/2008:15:17:53 +0200] [localhost/sid#80b9668][rid#8219ab8/initial/redir#1] (1) [perdir /var/www/apache2-default/] pass through /var/www/apache2-default/prueba.html.gz

Por cierto, si tenemos un fichero en un directorio y lo comprimimos con gzip el archivo original desaparecerá. Para comprimir un archivo manteniendo el original podemos hacerlo de la siguiente forma:

$ gzip -c style.css > style.css.gz
$ gzip -c prueba.html > prueba.html.gz

:wq

Entradas relacionadas

4 Comentarios a “Cómo servir contenidos comprimidos de forma estática con Apache”

Tema LHYLE09, creado por Vicente Navarro