Optimización AJAX 4: Caché permanente

En la parte final (por ahora) de esta serie sobre Optimización de respuestas AJAX vamos a revisar cómo utilizar técnicas de caché permanente, o lo que se debería denominar más correctamente caché-que-se-controla-desde-el-servidor — lo que complementa a la parte anterior sobre “caché volátil” o caché-que-se-controla-desde-el-cliente.

Cabeceras de control de caché

Cada vez que el cliente hace una petición al servidor web, éste envía junto al contenido un conjunto de cabeceras, que el navegador interpreta para activar un conjunto de comportamientos. En este sentido, una respuesta AJAX es como cualquier otra por lo que el navegador va a respetar las instrucciones indicadas en las cabeceras; de este modo podemos indicarle que guarde la respuesta en su caché y lea los datos desde el disco local, lo que evita enviar una nueva petición y acelera todo el proceso.

Por ejemplo, al hacer una solicitud a este sitio puedes ver las siguientes cabeceras:

felipe@laptop [08:56:14] [~]
-> % curl -I http://www.yukei.net/
HTTP/1.1 200 OK
Server: nginx
Date: Thu, 28 May 2015 23:56:18 GMT
Content-Type: text/html; charset=UTF-8
Cache-Control: public, must-revalidate, proxy-revalidate
Etag: 53836202edce31eae8555b8b7f0344ad
Last-Modified: Thu, 28 May 2015 23:50:24 GMT
Pragma: public
Connection: keep-alive
Vary: Accept-Encoding
X-Powered-By: W3 Total Cache/0.9.4.1
X-Pingback: http://www.yukei.net/xmlrpc.php

… lo cual es un set bastante típico y representativo de cabeceras. No son todas las que existen en el universo pero nos basta como muestra.

Existen varias cabeceras que están relacionadas con el control de caché, pero en general las podemos dividir en dos grupos:

  • Las que le indican al navegador que debe almacenar una copia de la página o archivo en el disco local y luego utilizar esa versión local en lugar de volver a solicitar el recurso.
  • Las que indican al navegador que debe utilizar la copia local de un recurso.

Revisemos las del primer grupo.

cache-control

Aunque por el nombre es bastante claro lo que hace esta cabecera, su funcionamiento es un poco menos intuitivo, ya que es suele ser compleja y tener múltiples valores:

  • public indica que la respuesta es válida para cualquier usuario, por lo que si existen cachés intermedios (por ejemplo, un proxy que en una red local también actúa como caché o “acelerador”, como es común encontrar en redes de universidades o grandes empresas) estos pueden guardar la respuesta para ser consumida por otros clientes. Piensa básicamente en cualquier cosa que se ve igual independiente de quién lo consulte; sin iniciar sesión. Lo contrario sería private, que indica que la respuesta es válida solamente para el cliente que hizo esa petición en particular y por lo tanto no se debería guardar en el proxy.
  • must-revalidate indica que cuando el cliente va a solicitar nuevamente el mismo recurso, debe volver a enviar la petición para verificar que el recurso no ha cambiado. Más adelante revisaremos cómo se hace esto con Etags y respuestas 304.
  • proxy-revalidate indica básicamente lo mismo, pero para los cachés intermedios.

Hay muchas otras directivas que se pueden incluir en esta cabecera; una de las más notables es max-age: {segundos} que indica el tiempo de expiración del recurso en segundos a partir de la petición original. Cuando se usa esta directiva no va a estar junto a must-revalidate ya que el servidor le está indicando al cliente que su copia es válida por “x” segundos y que no vuelva a solicitar el recurso hasta pasado ese periodo.

No hay una única versión ideal para la configuración de esta cabecera ya que su configuración va a depender de la naturaleza de la aplicación, los datos que se consultan, etc. Por ejemplo, si es información que se actualiza muy frecuentemente no vas a querer que el usuario quede con una copia antigua; pero si tiene un ritmo de actualización constante pero no tan frecuente y donde no es crítico que el usuario vea las actualizaciones del sitio podrías hacer un buen uso de max-age.

Esta cabecera es el estándar más reciente y reemplaza a las cabeceras que se utilizaban anteriormente.

De todos modos revisaremos algunas de las más comunes.

expires

Es la cabecera que más uso tenía antes de cache-control.

Es mucho más sencilla, ya que indica solamente un dato: la “fecha de vencimiento” del recurso solicitado por el cliente. En este sentido, es algo similar a la directiva cache-control: max-age pero con la diferencia que en lugar de indicar un tiempo de validez, señala una fecha de expiración. Es decir:

  • max-age indica “este contenido que fue solicitado ahora es válido por {xxx} segundos más”
  • expires indica “este contenido es válido hasta el {xxx} de {xxx} a las {xxx}”

Por ejemplo, en el sitio de un periódico en línea que se actualiza regularmente a determinadas horas, podrías usar esta cabecera con la fecha y hora de la próxima actualización. Pero ojo, que lo indicado por cache-control tiene más peso.

pragma y vary

Pragma cumplía la función de indicar si correspondía almacenar la respuesta en caché. Sus valores son similares a los que se usan en cache-control: public/private o no-cache

Vary resulta un poco más enigmática al punto que los distintos navegadores solían tener problemas con su implementación. Básicamente, indica para quién es válida la respuesta; algunos de sus valores son:

  • User-Agent, lo que significa que distintos navegadores podían obtener distintas versiones del mismo recurso, por ejemplo, si la versión móvil fuera distinta a la de escritorio.
  • Accept-Encoding se usa para verificar que el cliente reciba los datos en un formato que pueda entender. El caso típico es para determinar si el navegador puede recibir respuestas comprimidas con gzip.

Validación de caché con Etags

La cabecera ETags son un mecanismo para que el navegador determine si el recurso que está solicitando coincide con lo que tiene almacenado en caché, e invalidarlo si corresponde. Es decir, en lugar de comparar por fecha de modificación y expiración del caché, vas a comparar por el contenido de la respuesta, de modo que si el contenido de la respuesta cambia, el navegador debiese obtener inmediatamente la nueva versión.

Obviamente comparar la respuesta con sí misma no tiene mucho sentido, por ello el valor del ETag corresponde a un hash, para el cual no hay un formato estándar.

Cuando una respuesta incluye una cabecera ETag y el cliente decide guardarla en caché, al ejecutar una nueva solicitud a la misma URL el navegador agregará a la petición la cabecera If-None-Match con el valor de la cabecera ETag. Si el valor coincide, el servidor envía una respuesta con código 304 Not Modified, que puede contener cabeceras pero no tiene un cuerpo, ya que el navegador utilizará como contenido lo que tiene almacenado en caché.

Es decir, el diálogo es algo como:

  • CLIENTE: Hola servidor, dame el archivo index.html
  • SERVIDOR: Ok, aquí tienes el archivo. Puedes cachear este archivo con el identificador abcxyz → envía ETag
  • CLIENTE: Ok, lo almacenaré en caché según las órdenes que me has dado.
  • CLIENTE: Hola servidor, necesito nuevamente el archivo index.html; tengo una copia con el identificador abcxyz, ¿aún es válido? Envía If-None-Match
  • SERVIDOR: Sí, usa la copia que tienes. → Envía 304 Not Modified
  • CLIENTE: Ok.

Un ejemplo muy básico sería:

function yukei_in_cache( $etag ){
    // $_SERVER['HTTP_IF_NONE_MACH'] tiene el valor
    // del etag guardado por el navegador
    // si no lo envía, es porque no está en caché
    if ( ! isset( $_SERVER['HTTP_IF_NONE_MATCH']) ) {
        return false;
    }
    if ( $_SERVER['HTTP_IF_NONE_MATCH'] == $etag ){
        return true;
    }
}

function yukei_ajax_get_posts(){
    $paged = absint(  $_GET['paged'] );
    $posts = get_posts([ 'paged' => $paged ]);

    // voy a usar como hash los ids de los posts.
    // esto significa que si se ha publicado un nuevo
    // post, el hash/etag será distinto y se invalidarán
    // los datos en caché
    $ids  = wp_list_pluck( $posts, 'ID' );
    $etag = md5( json_encode($ids) );

    // la respuesta siempre debe incluir cabeceras
    // de control de caché
    header('ETag: '. $etag );
    header('Cache-Control: public, max-age: 3600');

    if ( yukei_ajax_posts_in_cache( $etag ) ) {
        // está en caché y es válido
        header('HTTP/1.1 304 Not Modified');
        exit;
    }

    // indicamos el tipo de contenido de la respuesta
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode( $posts );
    exit;
}

add_action('wp_ajax_yukei_get_posts', 'yukei_ajax_get_posts');
add_action('wp_ajax_nopriv_yukei_get_posts', 'yukei_ajax_get_posts');

Referencias

Puedes encontrar más información en los siguientes enlaces: