Todo lo que deberías saber sobre APC (II)

Configurar APC: directivas de configuración Aviso: si no has leído antes el primer artículo de la serie deberías hacerlo.

CONFIGURANDO APC para una óptima optimización optimizada

Como ya sabrás, las directivas de configuración se pueden cambiar en los ficheros .ini de configuración de PHP y sus extensiones. En el caso de Linux, probablemente el fichero esté en /etc/php5/apache2/conf.d/apc.ini, aunque también puedes añadir directivas en php.ini o cualquier otro fichero .ini del directorio conf.d/, ya que todos son cargados por el motor de PHP.

De todas las directivas de configuración que APC ofrece, creo que estas son las más jugosas:

  • apc.stat: indica si APC debe comprobar, a cada petición, si el archivo solicitado ha cambiado. En un servidor de producción es interesante desactivar esta opción ya que nos ahorraremos esa llamada al sistema. Pero ¡cuidado con los cambios de código! Para recompilar un script debes limpiar manualmente el APC o bien reiniciar Apache (con FastCGI no serviría).
  • apc.stat_ctime: comprueba que un archivo ha cambiado a través de los inodes, en lugar de con mtime.
  • apc.shm_size: especifica la memoria reservada para APC, en MB. Por defecto es 30.
  • apc.max_file_size: tamaño máximo de script cacheable.
  • apc.lazy_functions y apc.lazy_classes: cargan las funciones y clases de los includes sólo cuando son necesarios.
  • num_files_hint: aproximadamente cuántos scripts tienes en tu servidor. Ayuda a APC a optimizar sus índices internos. Si no estás seguro, mejor dejarlo a 0.
  • user_entries_hint: aproximadamente cuántas entradas de usuario guardarás. No estás seguro, mejor déjalo en 0.
  • apc.cache_by_defaultapc.filters: si pones apc.cache_by_default = 1, APC sólo cacheará los scripts cuyo nombre coincida con la expresión regular que especifiques en apc.filters. Interesante si tienes algunos scripts que sabes que van a cambiar muy frecuentemente (por ejemplo, durante un período de actualización) y has sido tan machote de desactivar apc.stat.
  • apc.include_once_override: se trata de una característica experimental, que se supone que optimiza las llamadas include_once y require_once para que consuman menos recursos. Dentro de algún tiempo tendremos más información sobre esta funcionalidad.

La extraña pero maravillosa directiva apc.rfc1867

Entre todas estas directivas de configuración destaca rfc1867, que obviamente hace referencia al documento RFC 1867, el cual especifica la subida de ficheros por HTML/HTTP. Pero ¿esto a qué viene?

Pues viene a que con APC, además de cachear los opcodes y guardar valores en la cache, también puedes controlar el progreso de las subidas de ficheros.

Sí, amigos. Si activamos la directiva apc.rfc1867, por cada fichero que se suba a tu servidor APC creará e irá actualizando una user entry con información sobre la subida. Esta entrada contendrá algo así como:

Array
(
    [total] => 1142543
    [current] => 1142543
    [rate] => 1828068.8
    [filename] => test
    [name] => file
    [temp_filename] => /tmp/php8F
    [cancel_upload] => 0
    [done] => 1
)

Es decir, el tamaño total del fichero, los bytes transferidos (current), la velocidad de subida (rate) y otros parámetros. Con esta valiosa información puedes hacer una bonita barra de progreso para indicar a tus usuarios cómo van sus subidas. Si quieres más detalles sobre cómo utilizar esta característica, te remito a este artículo y, como siempre, al manual oficial.

Si te has quedado con ganas de más

  • apc@facebook: presentación de 2007 donde se explica el funcionamiento de APC y se exponen los valores de configuración en los servidores de Facebook. Además explican su interesantísimo sistema para distribuir configuraciones en múltiples servidores a través de APC.
  • 2bits cuentan un caso interesante con Drupal y la discusión es también muy rica.
  • ¡El manual! No me canso de decirlo.

Esto no termina aquí

Te toca a ti. ¿Tienes experiencia con APC? ¿Has notado la mejora de rendimiento en tus servidores de producción? ¿Cómo lo has configurado? ¿Me das la contraseña de root?

Todo lo que deberías saber sobre APC (I)

Una de las formas más fáciles, a la vez que efectivas (un 300% de media), de hacer que tu sitio web PHP sea más rápido es usar un opcode cache. Hay varios en el mercado: APC, eAccelerator, XCache… vamos a ver APC por ser libre, el más fácil de manejar, extendido y el que mejores resultados arroja en los benchmarks.

Pero… ¿qué es un opcode cache?

Diagrama de flujo de APCComo seguramente sabrás, PHP es un lenguaje interpretado. Esto significa que cada vez que se solicita la ejecución de un script (o sea, cuando el usuario navega a una página concreta), este se compila y se ejecuta de forma automática, dando la sensación de que “no hay que compilar PHP”. Es un mito bastante extendido que PHP no se compila. Falso. PHP sí se compila, pero de forma automática. “Just In Time” (JIT), que dicen los anglosajones.

Este sistema de compilación JIT tiene la ventaja de una gestión más simple, sobre todo cuando tienes un script ya funcionando en el servidor y quieres modificarlo. Con PHP simplemente tienes que sustituir el fichero… y listo, la próxima vez que alguien acceda ejecutará la nueva versión.

Pero también tiene una desventaja: a cada visita se debe compilar el script. Y es una pérdida de tiempo, ya que si todos los visitantes de tu web ejecutan el mismo script, ¿para qué compilarlo una y otra vez? ¿No sería mejor compilarlo la primera vez, guardar ese script compilado (opcode) y ejecutarlo cuando alguien lo solicite?

Pues eso es exactamente lo que hacen los opcode cache: compilan el script la primera vez, lo guardan en memoria (RAM), y cuando se solicita, simplemente ejecutan el script compilado, logrando un aumento de velocidad bastante considerable.

Me has convencido. ¡Usemos APC!

APC (Another PHP Cache) es un sistema automático que compila los script sólo cuando es necesario. Viene empaquetado como una extensión de PHP, así que su instalación es sencilla. Y más si estamos usando Ubuntu o Debian Linux:

$ sudo apt-get install php-apc

Con sólo instalar este paquete (suponiendo que ya tengamos instalados Apache y PHP) ya tendremos APC funcionando correctamente. Si quieres más información sobre la instalación de APC puedes ir a la documentación oficial o en Google. Si tu proveedor de hosting no te ofrece APC… lo siento, es lo que tiene el hosting compartido :-P

Para saber si APC está funcionando correctamente, ejecutamos phpinfo():

APC como cache genérico

Además de “cachear” los scripts, APC sirve como almacenamiento genérico en memoria. Esto quiere decir que puedes guardar datos en la memoria de APC que estarán en RAM (por tanto, será de más rápido acceso que si los guardas en el disco duro o en una base de datos) y que podrás recuperar en cualquier momento. Eso sí, recuerda que si reinicias Apache o el sistema operativo, todos los datos que APC haya guardado en memoria se eliminarán.

Para almacenar una variable en la cache de APC puedes usar la función apc_add(), y para leerla, apc_fetch(). Por ejemplo:

/*
 * AVISO: este código tiene fines exclusivamente didácticos, y no debe ser
 * utilizado en aplicaciones reales en producción
 */

function leerPerfil() {
    $idUsuario = intval($_GET['idUsuario']);

    /**
     * Lee de la BD los datos del usuario $idUsuario. Si los datos están en cache,
     * no hacemos la consulta. Si no están, hacemos la consulta y guardamos los
     * datos en cache
     */
    if (empty($datosUsuario = apc_fetch("perfil_$idUsuario"))) {
        $query = mysql_query("SELECT * FROM usuarios WHERE id = '$idUsuario'");
        $datosUsuario = mysql_fetch_assoc($query);
        apc_add("perfil_$idUsuario", $datosUsuario);
    }

    return $datosUsuario;
}

Este sistema te permite hacer optimizaciones adicionales en tus aplicaciones (por ejemplo, guardar el resultado de una consulta a la base de datos para no tener que hacerla dos veces), pero tendrás que currártelas a mano. Tú eliges qué almacenar en APC. Pero ojo, si vas a hacer un uso intensivo de la caché, es mejor que uses un sistema de caché diferente al de los scripts. Por ejemplo, el famoso memcached (usado por sitios como Google o Facebook), que es un sistema de caché genérico, muy eficiente y muy fácil de usar (en serio, es muy fácil). Más información sobre funciones de APC en el manual de PHP.

Monitorizar APC

Otra de las ventajas de APC es que tiene un precioso panel de control. Es un script PHP al que puedes acceder haciendo un enlace desde tu directorio www público hasta la ubicación del script (en los directorios del sistema de PHP):

gunzip -c /usr/share/doc/php-apc/apc.php.gz > /var/www/apc.php

Si no funciona, o si no usas Linux puedes leer la documentación para instalar el panel de control de APC. Si no encuentras el script en tu sistema de ficheros puedes sencillamente descargarlo desde el Subversion de APC.

Aviso de seguridad: este script permite ver el estado de APC, revelando alguna información sobre el servidor que puede ser sensible por temas de seguridad. Es tu responsabilidad proteger el acceso al panel de control a través de configuraciones del servidor web, etc.

El panel de control tiene algunas opciones “públicas” y otras a las que sólo puedes acceder iniciando sesión con nombre de usuario y contraseña. Para cambiar el nombre de usuario y la clave puedes editar el propio script del panel de control, apc.php:

Cambiar la contraseña al panel de control de APC

El panel de control nos ofrece tres apartados:

  1. View Host Stats, donde tenemos un cuadro de control de APC con gráficas de consumo de memoria, información del sistema y datos de uso del cache.
  2. System Cache Entries: la lista de scripts cacheados, así como el número de hits (ejecuciones) que han tenido, su tamaño y fechas de último acceso/última modificación/creación.
  3. User Cache Entries: las variables que se hayan guardado en APC.

Configurar APC: panel de control

En el próximo artículo explicaremos más detalles sobre el panel de control de PHP y cómo interpretar los datos que nos brinda de cara optimizar aún más nuestro sitio web. Aunque si has llegado hasta aquí y has instalado APC en tu servidor ya has conseguido un gran ahorro de recursos en tu servidor :-)

Ajustando la cache de consultas de MySQL para un mejor rendimiento de Drupal

¡Hola de nuevo! Después de un tiempo de inactividad voy a retomar el blog, aunque no prometo publicar muy asiduamente :-P

Continuando el hilo post anterior (mayo de 2010, ¡la de cosas que han pasado desde entonces!), vamos a ver cómo aumentar el rendimiento de un sitio Drupal, en este caso a través de la cache de consultas de MySQL.

En /admin/reports/status/sql, Drupal muestra algunas estadísticas interesantes del servidor MySQL, que nos dan pistas sobre lo bien o mal optimizado que tenemos el sistema: número de consultas cacheadas, ordenaciones sin índices, etc.

En la explicación de alguno de los indicadores se indica “debe ser cero”. Estos indicadores son Select_full_join, Select_range_check y Sort_scan. En términos generales, si estos valores son altos se debería modificar el esquema de la BD añadiendo índices, aunque si estamos hablando de Drupal esta situación no debería darse, ya que el esquema tiene índices correctamente establecidos.

Otro indicador interesante es Qcache_lowmem_prunes. Indica “el número de veces que MySQL tuvo que retirar consultas del caché porque se quedó sin memoria”. En mi caso, este indicador mostraba el alarmante valor de 349102 (¡desde que se arrancó el servidor!).

Para arreglar esta situación vamos a aumentar el tamaño asignado a la caché de consultas. Lo haremos desde el fichero de configuración de MySQL (my.cnf):

query_cache_limit = 1M
query_cache_size = 32M

El primer parámetro es el tamaño máximo de las consultas “cacheables”. Lo usaremos para evitar cachear consultas lentas o muy grandes, que suelen ser infrecuentes (y por tanto no tiene mucho sentido cachearlas).

El segundo parámetro es la cantidad de memoria que queremos asignar a la caché. Cuanta más memoria RAM tenga nuestro servidor, mayor valor podremos darle. En mi caso el servidor tiene 512MB, de los cuales alrededor de 200 están ocupados por el sistema y los servicios (Apache, el propio MySQL, memcache y los servicios de correo). Así que puedo aumentar el tamaño del cache hasta 50MB.

Después de reiniciar el servidor, lleva 95 días funcionando y arroja un valor de 3451 purgas. Muchas menos que con la anterior configuración.

Esta es una medida sencilla que ayuda a optimizar la cache de consultas de MySQL, un sistema que impacta en el rendimiento global del sistema, sobre todo si no tienes un sistema de caché fuera de MySQL.

Mejor rendimiento de Drupal con GraphicsMagick

Un pequeño (y espero útil) apunte antes de que penséis que he abandonado completamente el blog :-(

Se trata de un consejo para mejorar el rendimiento de nuestro sitio Drupal. El sistema utiliza por defecto la biblioteca GD para manipular imágenes (por ejemplo, generar miniaturas). El módulo ImageAPI inserta una capa de abstracción en Drupal que permite elegir qué biblioteca gráfica usar, GD o ImageMagick. Es mejor utilizar ImageMagick, ya que es una biblioteca independiente de PHP y, por ello obtendremos mejor rendimiento total en Drupal, y además es más estable (si falla GD puede caer todo el motor de PHP, sin embargo, si falla ImageMagick no caerá, ya que es un binario diferente).

Pero aún mejor: existe un proyecto llamado GraphicsMagick, que es un fork de ImageMagick y que es más estable y, sobre todo, con mejor rendimiento, ya que consume menos memoria y es más rápida. Y lo que es mejor, tiene la misma API que ImageMagick (el comando

convert), con lo cual podemos engañar a ImageAPI diciéndole que use ImageMagick cuando en realidad tenemos instalado GraphicsMagick.

Para lograr esto no tienes más que instalar GraphicsMagick y el módulo ImageAPI de Drupal, en dos sencillos pasos. Supongamos que trabajamos con un servidor Debian/Ubuntu (en otros sistemas operativos no debería ser muy diferente):

  1. Instalamos GraphicsMagick:
    apt-get install graphicsmagick-imagemagick-compatPodemos probar que GraphicsMagick funciona escribiendo convert en la consola:

isra@salon:~$ convert
GraphicsMagick 1.3.5 2009-01-26 Q8 http://www.GraphicsMagick.org/
Copyright (C) 2002-2009 GraphicsMagick Group.
Additional copyrights and licenses apply to this software.
See http://www.GraphicsMagick.org/www/Copyright.html for details.

Usage: convert [options ...] file [ [options ...] file …] [options ...] file

[más opciones]

  • Instalamos el módulo ImageAPI y lo activamos en /admin/build/modules (módulos ImageAPI e ImageAPI ImageMagick).
  • Accedemos a la configuración de ImageAPI en Administración > Configuración del sitio > ImageAPI (/admin/settings/imageapi) y veremos el siguiente mensaje: “The ImageAPI ImageMagick module is the only enabled image toolkit. Drupal will use it for resizing, cropping and other image manipulations“.
  • Si también hemos activado el módulo ImageAPI GD2, nos dará a elegir (seleccionamos ImageAPI ImageMagick como opción por defecto):

¡Y listo! Fácil y rápido. Podemos comprobar que funciona accediendo a Administración > Configuración del sitio > ImageAPI > Configurar (/admin/settings/imageapi/config), lo que nos mostrará información sobre GraphicsMagick (versión, tipos soportados, etc):

¿He hecho benchmarks para comprobar la ganancia de rendimiento con respecto a ImageMagick?

Pues no, lo siento. Pero los chicos de GraphicsMagick sí los han hecho, y son bastante jugosos.

Pero… ¿no es necesaria la extension imagick de PHP?

No, no es necesaria, ya que ImageAPI interactúa con ImageMagick/GraphicsMagick a través del binario
convert

Iré escribiendo algunos artículos más sobre técnicas fáciles para optimizar el rendimiento de Drupal.

Aún más rendimiento con CssDispatcher

En el artículo “Maneja las CSS como un profesional” veíamos algunas técnicas para insertar variables y funciones en las hojas de estilo, así como posibles mejoras del rendimiento en el cliente a la hora de servir las CSS, y para ello utilizábamos la biblioteca CssDispatcher.

Pues bien, en este post vamos a ver varias soluciones para aumentar el rendimiento en el servidor cuando servimos hojas de estilos, especialmente si son hojas de estilos tratadas con CssDispatcher.

Es importante diferenciar las mejoras de rendimiento en el cliente y el servidor. Mientras los benchmarks nos dicen que el 80% del tiempo de carga de una página lo causa el cliente (transferencias, renderizado, etc), para nosotros también es importante ese otro 20%, porque genera una carga en el servidor (acceso a disco duro, procesador, memoria…) que puede ponernos en aprietos si las visitas aumentan.

Accesos al disco duro

De por sí, servir una hoja de estilos simple no suele ser muy costoso para un servidor web. Si un cliente solicita /main.css, el servidor abre el archivo, lo lee y lo devuelve. Además, si el sistema operativo implementa caché de ficheros, 1000 visitas no generarán 1000 accesos al disco duro, lo cual aumentará la velocidad y reducirá la carga del servidor.

Si queremos ir más allá y acelerar aún más los accesos a disco duro podemos poner los archivos del servidor un sistema de archivos montado sobre la memoria RAM. Es decir, una porción del sistema de archivos que, en lugar de guardarse en el disco duro, lo hace sobre una porción de la memoria RAM. No es una técnica muy recomendable, porque aunque no es difícil montarlo, los datos desaparecen al apagar o reiniciar la máquina, por lo que habría que tener sincronizados los directorios en memoria con otros en el disco duro para que no se perdiesen.

Procesamiento de la petición

Como ya he dicho antes y todo el mundo sabe, para el servidor web devolver un archivo estático no le cuesta más que abrirlo e irlo enviando por el socket. Pero si la hoja de estilos pasa por el compilador de PHP (por introducir códigos PHP en la hoja de estilos) entonces la cosa se complica un poco. Para acelerar el procesado de scripts es recomendable usar:

  • Una caché de salida, que evita tener que ejecutar repetitivamente el mismo código para cada petición.
  • Un opcode cache, que evita tener que compilar el código para cada petición (si hemos implementado caché de salida, para la mayoría de las peticiones no será necesario ni compilar ni ejecutar el código).

Diagramas de flujo para el despacho de peticiones con caché

Para la opcode cache existen herramientas como APC, XCache o eAccelerator, que una vez instaladas y configuradas debidamente funcionan sin que tengamos que modificar el código de nuestra aplicación web. Puedes leer más sobre opcode cache en PHP Accelerators.

En cuanto a la caché de salida, hay principalmente dos caminos a seguir: gestionar la caché desde el servidor web o desde PHP. La ventaja del primero es que, al no ejecutar código PHP es un poco más rápido. Esto se puede lograr con el módulo Apache mod_cache. La otra posibilidad, gestionarlo con PHP, nos da más flexibilidad y control sobre la solución. En concreto, podremos guardar la caché en el disco duro o en memoria, con gestores de caché como Memcached (mi preferido).

Caché con URL propia

Existe una tercera solución, y es utilizar una caché en disco duro sin procesar las peticiones por PHP. El funcionamiento es el siguiente: si tenemos una hoja de estilos generada con CssDispatcher en/estilos.css.php, podemos guardar la salida en un archivo y ponerlo accesible en/estilos.css. Al ser un archivo estático, se servirá tan rápido como una hoja de estilos normal. Este sistema tiene un problema, y es que si tenemos hojas de estilos específicas para navegadores, no resultará fácil servir una u otra de forma estática. La única solución aparente es utilizar redirecciones y estructuras condicionales en los ficheros de configuración de Apache.

Algo de código, por favor

Vamos a implementar tres sistemas de caché: uno con Memcached, otro en disco y otro en disco accesible directamente.

Cache en Memcache

<?php

include 'class.Css.php';

//Nombre del elemento en Memcache
$cache_name = 'moduleName_' . Css::getUserAgent();
//Tiempo de vida. Para archivos que no cambien mucho, poner valores altos
$cache_time = 3600*24; 

//Conexión a Memcache
$mc = new Memcache();
$mc->connect('localhost');

//Si la salida no está en caché, se genera y se almacena
if (!$out = $mc->get($cache_name)) {

    //Incluye la biblioteca CssDispatcher.
    //No se incluye si no es necesaria
    include 'class.CssDispatcher.php';

    $estilos = new CssDispatcher;

    //Crea una nueva hoja de estilos
    $general = new Css('example.css.php');
    //Asigna variables
    $general->background = '#eee';
    $general->border_color = 'red';
    $general->header_size = 2.1;

    //Crea otra hoja de estilos para navegadores WebKit
    $another = new Css('example2.css.php', Css::UA_WEBKIT);
    $another->bold = 'font-weight: bold';

    //Añade las hojas de estilo al dispatcher
    $estilos->add($general);
    $estilos->add($another);

    //Añade un aviso
    $out = '/* Generated by CssDispatcher ' . strftime('%c') . " */n"
        . $estilos->render(false, true, false, true);

    //Guarda la salida en Memcache
    $mc->set($cache_name, $out, null, $cache_time);
}

header("Content-Type: text/css");
echo $out;
die();

?>

Tiempo de ejecución (renovando la caché): 3.6480ms
Tiempo de ejecución (utilizando la caché): 0.6439ms

Caché en disco duro

Este script tendrá será más lento y pesado para la CPU que el anterior, pero no necesita de servicios adicionales como Memcached, imposibles de instalar en alojamientos compartidos.

<?php

include 'class.Css.php';

//Archivo de caché
$cache_name = 'cssCache/moduleName_' . Css::getUserAgent();
//Tiempo de vida, poner valores altos para archivos que no suelen cambiar
$cache_time = 30; 

if (file_exists($cache_name)) {
    if (date('U') - fileatime($cache_name) > $cache_time) {
        $regenerate = true;
    } else {
        $out = file_get_contents($cache_name);
    }
} else {
    $regenerate = true;
}

//Si la salida no está en caché, se regenera
if ($regenerate) {

    //Carga la biblioteca CssDispatcher
    include 'class.CssDispatcher.php';

    $estilos = new CssDispatcher;

    //Crea una nueva hoja de estilos
    $general = new Css('example.css.php');
    //Asigna variables
    $general->background = '#eee';
    $general->border_color = 'red';
    $general->header_size = 2.1;

    //Añade otra hoja de estilos para navegadores WebKit
    $another = new Css('example2.css.php', Css::UA_WEBKIT);
    $another->bold = 'font-weight: bold';

    //Añade las plantillas al dispatcher
    $estilos->add($general);
    $estilos->add($another);

    $out = '/* Generated by CssDispatcher ' . strftime('%c') . " */n"
        . $estilos->render(false, true, false, true);

    //Graba la salida en el fichero
    file_put_contents($cache_name, $out);
}

header("Content-Type: text/css");
echo $out;
die();

?>

Tiempo de ejecución (renovando caché): 1.290ms
Tiempo de ejecución (utilizando caché): 0.242ms

El último caso, una caché en ficheros accesibles públicamente, es más rápida y más ligera que la anterior, pero nos añade la tarea de actualizar la caché cuando nos interese, bien con una tarea programada (cron) o mediante algún otro mecanismo.

<?php

include 'class.Css.php';
include 'class.CssDispatcher.php';

//Generamos hojas de estilos para todos los navegadores
$navegadores = array(Css::UA_IE6, Css::UA_IE, Css::UA_GECKO, Css::UA_WEBKIT);

//Para cada navegador generamos la hoja de estilos correspondiente
foreach ($navegadores as $navegador) {
    //Archivo de caché
    $cache_name = 'cssCache/estilos_' . $navegador . '.css';
    //Tiempo de vida. Para archivos que no cambien mucho, poner valores altos
    $cache_time = 30; 

    $estilos = new CssDispatcher;

    //Crea una nueva hoja de estilos
    $general = new Css('example.css.php');
    //Asigna variables
    $general->background = '#eee';
    $general->border_color = 'red';
    $general->header_size = 2.1;

    //Añade las plantilla al dispatcher
    $estilos->add($general);

    //Hoja de estilos para WebKit
    //No vale utilizar la detección de navegador de CssDispatcher
    if ($navegador == Css::UA_WEBKIT) {
        $another = new Css('example2.css.php');
        $another->bold = 'font-weight: bold';
        $estilos->add($another);
    }

    $out = '/* Generated by CssDispatcher ' . strftime('%c') . " */n"
        . $estilos->render(false, true, false, true);

    //Graba la salida en el fichero
    file_put_contents($cache_name, $out);
}

echo "Caché de CSS actualizada";

?>

 

Tiempo de ejecución del actualizador de caché: 4.009ms
Tiempo de obtención de la hoja de estilos: 86ms (incluye la transmisión del fichero)

Ya que con este método no tenemos un sólo punto de entrada para las diferentes hojas de estilos, será necesario cargar una u otra CSS en función del navegador, a través de los artificios clásicos: comentarios condicionales o la cabecera User-Agent. Pero tendremos que hacerlo manualmente, ya que al usar ficheros estáticos no se ejecuta CssDispatcher. Por ejemplo, según el ejemplo, si el navegador es Firefox tendremos que invocarcssCache/estilos_20.css.

Por mi parte, es todo por hoy. ¿Conoces alguna otra técnica para aumentar el rendimiento al servir CSS? ¿Has probado a cachear la salida de CssDispatcher?