Estrategias avanzadas de cache para WordPress 2026: Más allá de archivos estáticos

Estrategias avanzadas de cache para WordPress 2026: Más allá de archivos estáticos

Última verificación: 13 de octubre de 2026
28 min de lectura
Guía
Core Web Vitals
Desarrollador full-stack

En 2026, la brecha de rendimiento entre un sitio WordPress “estándar” y uno “empresarial” está definida por la inteligencia del cache. Mientras que el cache básico de páginas era suficiente hace una década, las aplicaciones web modernas requieren estrategias de cache multicapa que entiendan qué cachear, dónde cachearlo y precisamente cuándo invalidarlo.

Hace diez años, el cache era simple: se guardaba una página como archivo HTML y se servía hasta que expiraba. En 2026, ese enfoque de todo o nada ya no es suficiente. Los sitios web modernos son entidades híbridas: parte estáticas, parte dinámicas, servidas globalmente a usuarios con velocidades de conexión y dispositivos variados. Para alcanzar 100/100 en Core Web Vitals mientras se sirve contenido personalizado, necesitas una estrategia que optimice cada capa de la pila de entrega.

La arquitectura práctica es por capas: cache del navegador, edge de la CDN, cache de página completa, cache de fragmentos, cache de objetos e higiene de la base de datos, trabajando juntas en lugar de competir entre sí.

Conoce más sobre la optimización de velocidad WordPress en WPPoland.

#Cómo funciona el cache edge de tres capas en WordPress

En 2026, tu servidor de origen casi nunca recibe una solicitud de visitante directamente. Las implementaciones modernas de WordPress siguen una estrategia de cache de triple capa que distribuye el contenido por todo el mundo.

#Capa 1: cache estático en el edge (CDN)

HTML, imágenes, CSS y JavaScript se almacenan en el borde de la red a través de servicios como Cloudflare, Bunny.net o Fastly. Esto proporciona el bajo Time to First Byte (TTFB) que los usuarios esperan.

Configuraciones clave:

  • Cache Everything: cachear HTML en el edge, no solo activos estáticos
  • Edge TTL: establecer tiempos de vida de cache apropiados (1 hora para dinámico, 1 año para estático)
  • Browser TTL: controlar las cabeceras de cache del lado del cliente
  • Always Online: servir contenido obsoleto si el origen no está disponible

Ejemplo de Page Rules en Cloudflare:

*example.com/*
Cache Level: Cache Everything
Edge Cache TTL: 2 hours
Browser Cache TTL: 30 minutes
Always Online: On

Cada milisegundo cuenta. Un TTFB de 200 ms frente a uno de 50 ms no parece mucho en números absolutos, pero multiplicado por millones de solicitudes y sumado al impacto en Core Web Vitals, la diferencia se nota en posiciones de búsqueda y tasas de conversión.

#Capa 2: cache de respuestas API

Para configuraciones WordPress headless o desacopladas, las respuestas API se cachean por separado del HTML:

  • Endpoints REST API cacheados con TTL corto (5-15 minutos)
  • Consultas GraphQL cacheadas según su complejidad y la volatilidad de los datos
  • Purga de cache activada por webhooks para actualizaciones instantáneas

Beneficios:

  • Reduce la carga de base de datos para frontends impulsados por JavaScript
  • Permite experiencias móviles más rápidas
  • Soporta funciones en tiempo real con fallbacks cacheados

Esta capa es especialmente crítica para sitios que utilizan arquitecturas headless con Astro o Next.js, donde el frontend consume datos a través de APIs y cada milisegundo de latencia en la respuesta API se traduce directamente en una peor experiencia de usuario.

#Capa 3: cache locking y protección contra thundering herd

Cuando un elemento cacheado expira, las configuraciones tradicionales permiten que múltiples solicitudes simultáneas golpeen el servidor de origen. En 2026, implementamos cache locking:

  • Solo la primera solicitud regenera el contenido cacheado
  • Las solicitudes siguientes reciben una versión ligeramente obsoleta
  • Una vez completada la regeneración, todos los usuarios reciben contenido fresco

Implementación con Cloudflare Workers:

// Cache locking pattern
async function handleRequest(request) {
  const cache = caches.default;
  const cacheKey = new Request(request.url);

  let response = await cache.match(cacheKey);

  if (!response) {
    // Check for regeneration lock
    const lockKey = `lock:${request.url}`;
    const lock = await CACHE_LOCKS.get(lockKey);

    if (lock) {
      // Serve stale while regenerating
      response = await cache.match(`${cacheKey}:stale`);
    } else {
      // Acquire lock and regenerate
      await CACHE_LOCKS.put(lockKey, 'locked', {expirationTtl: 30});
      response = await fetchOrigin(request);
      await cache.put(cacheKey, response.clone());
      await cache.put(`${cacheKey}:stale`, response.clone());
    }
  }

  return response;
}

El problema del “thundering herd” (estampida) es especialmente grave en sitios de alto tráfico. Imagina un sitio con miles de visitantes concurrentes donde expira el cache de la página principal: sin protección, todas esas solicitudes golpearían a la vez el servidor de origen. Con cache locking, solo una solicitud llega al origen mientras las demás reciben la versión anterior del cache.

#Cache de página completa con Nginx FastCGI y Varnish

Las redes edge entregan rápidamente activos estáticos y HTML público, pero el origen sigue necesitando un cache de página completa eficiente antes de que se ejecute PHP. El cache FastCGI de Nginx guarda las respuestas HTML renderizadas en zonas de memoria compartida y en disco local, de modo que los visitantes anónimos no pasan por los workers de PHP-FPM:

# FastCGI cache configuration in nginx.conf
fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:256m inactive=1d max_size=4g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

# Purge location using fastcgi_cache_purge
location ~ /purge(/.*) {
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
    fastcgi_cache_purge WORDPRESS "$scheme$request_method$host$1";
}

Cuando los editores actualizan contenido, un plugin de purga envía una petición HTTP PURGE al endpoint local de Nginx y se elimina la entrada de esa clave de cache. La documentación del módulo FastCGI de nginx indica que los nombres de los archivos de cache son el MD5 de la clave, y presenta fastcgi_cache_purge como parte de la suscripción comercial, por lo que las versiones open source de Nginx usan el módulo de terceros ngx_cache_purge para la misma tarea. La misma página documenta dos directivas que conviene activar en cualquier origen WordPress con tráfico: fastcgi_cache_lock, que permite que una sola petición a la vez rellene un nuevo elemento de cache, y fastcgi_cache_use_stale, que sirve una respuesta obsoleta cuando el backend FastCGI falla. En entornos con varios servidores y Varnish, las peticiones PURGE o las expresiones BAN eliminan entradas por patrón de URL o por cabeceras de etiquetas emitidas por plugins de WordPress.

#Cómo purgar el cache de WordPress por etiquetas

El problema más grande del cache empresarial siempre ha sido el “problema de la purga”. Corriges un error tipográfico en la página principal, y todo el cache del sitio se elimina, causando una caída del rendimiento y sobrecarga del servidor de origen.

En 2026, resolvemos esto con cache tagging.

#Cómo funciona el etiquetado de caché

En lugar de tratar el contenido cacheado como archivos aislados, etiquetamos cada elemento cacheado con metadatos sobre sus dependencias:

Homepage: tags=["home", "post_123", "post_124", "category_news"]
Blog Post: tags=["post_125", "category_tutorials", "author_john"]
Category Page: tags=["category_tutorials", "post_125", "post_126"]

Cuando actualizas “Post A”, solo se purgan los elementos etiquetados con post_a:

  • El post en sí mismo
  • Sus páginas de archivo de categoría
  • Los widgets de posts relacionados
  • Las páginas de archivo del autor
  • La página principal (si está destacado)

El resto de tus 10.000 páginas permanecen cacheadas y rápidas. Esta granularidad es lo que separa a los sitios WordPress empresariales de los promedio en rendimiento sostenido.

#Implementación de cache tags en WordPress

Con Cloudflare:

// Add cache tags to Cloudflare
function add_cache_tags_header() {
    if (is_single()) {
        $tags = [
            'post_' . get_the_ID(),
            'author_' . get_the_author_meta('ID'),
        ];

        // Add category tags
        $categories = get_the_category();
        foreach ($categories as $cat) {
            $tags[] = 'category_' . $cat->slug;
        }

        header('Cache-Tag: ' . implode(',', $tags));
    }
}
add_action('template_redirect', 'add_cache_tags_header');

Purga por etiqueta:

// Purge specific tags when post is updated
function purge_post_cache_tags($post_id) {
    $tags = [
        'post_' . $post_id,
        'author_' . get_post_field('post_author', $post_id),
    ];

    // Cloudflare API purge by tag
    $api = new Cloudflare\API\Client($email, $api_key);
    $api->zones()->cachePurgeTags($zone_id, $tags);
}
add_action('save_post', 'purge_post_cache_tags');

#Qué etiquetas de cache purgar cuando cambia un post

Jerarquía de etiquetas:

  • post_{id}: contenido individual del post
  • category_{slug}: páginas de archivo de categoría
  • author_{id}: páginas de archivo del autor
  • taxonomy_{name}_{term}: páginas de taxonomía personalizada
  • widget_{id}: contenido de widgets dinámicos
  • global: elementos de todo el sitio (encabezado, pie, menús)

Purga condicional:

// Only purge what changed
function smart_cache_purge($post_id) {
    $post = get_post($post_id);
    $tags = ['post_' . $post_id];

    // If featured image changed, purge image-related caches
    if (did_post_thumbnail_change($post_id)) {
        $tags[] = 'featured_images';
    }

    // If post status changed, purge archive listings
    if ($post->post_status !== get_previous_post_status($post_id)) {
        $tags[] = 'category_' . get_primary_category($post_id);
        $tags[] = 'author_' . $post->post_author;
    }

    purge_cache_tags($tags);
}

La jerarquía de etiquetas debe diseñarse con cuidado para equilibrar granularidad y complejidad. Demasiadas etiquetas crean sobrecarga administrativa; muy pocas resultan en purgas demasiado amplias que anulan el propósito del sistema.

#Redis object cache para WordPress en 2026

La base de datos es el cuello de botella de WordPress. Cada carga de página dispara docenas de consultas SQL para obtener posts, metadatos, opciones y datos de usuario. El object caching almacena los resultados de estas consultas en memoria, eliminando búsquedas redundantes en la base de datos.

#Por qué Redis 8+ en 2026

Las notas de la versión Redis Open Source 8.0 (mayo de 2025) indican que Redis Search, JSON, series temporales y cinco estructuras de datos probabilísticas, antes módulos separados, ahora forman parte del propio Redis. Para WordPress, eso significa que un único servidor puede alojar el object cache y también los datos que usan las funciones de búsqueda o de métricas:

Características de Redis 8:

  • JSON: almacenar y consultar objetos JSON complejos
  • Redis Search: búsqueda de texto completo dentro de datos cacheados
  • Series temporales: rastrear métricas de rendimiento del cache
  • Opciones de persistencia: RDB y AOF para durabilidad de datos

Redis frente a Memcached (comparación 2026):

CaracterísticaRedis 8+Memcached
Tipos de datos8+ (strings, hashes, listas, sets, sorted sets, JSON, streams)1 (strings)
PersistenciaRDB + AOFNinguna
ReplicaciónMaster-slave + ClusterNinguna
Eficiencia de memoriaMejor con compresiónBuena
Invalidación por etiquetasManual (por ejemplo, sets de claves)Implementación manual
Capacidades de consultaRedis SearchNinguna

#Drop-in de Redis frente a Memcached y mecánica del object cache

El núcleo de WordPress ofrece una interfaz de cache intercambiable a través de wp-content/object-cache.php. Sin un drop-in de object cache, WordPress usa un cache en memoria que solo dura lo que dura una petición PHP. Con un drop-in persistente (por ejemplo el que instala el plugin Redis Object Cache, o un drop-in de Memcached), llamadas como wp_cache_set(), wp_cache_get(), wp_cache_delete() y wp_cache_add() las atiende el servidor Redis o Memcached y sobreviven entre peticiones. La referencia de WP_Object_Cache lo dice claramente: “Por defecto, el object cache no es persistente”, de modo que los datos viven “solo en memoria y solo durante la petición” (traducción propia).

Es habitual confundir los transients (set_transient(), get_transient()) con las llamadas directas al object cache (wp_cache_set(), wp_cache_get()):

  • Cuando hay un drop-in de object cache externo activo, set_transient() se convierte directamente en wp_cache_set() dentro del grupo transient, sin tocar la tabla wp_options.
  • Cuando no hay drop-in persistente, los transients se escriben en wp_options como filas (_transient_* y _transient_timeout_*). En el código de set_transient() del núcleo, un transient guardado con expiración recibe una fila de timeout y no se carga automáticamente, mientras que uno guardado sin expiración se carga en cada petición, así que los transients sin expiración inflan poco a poco las opciones autocargadas.
  • El manual de la Transients API limita la clave a 172 caracteres y cita a Ryan McCue: “los tiempos de expiración de los transients son un tiempo máximo. No hay edad mínima” (traducción propia). El código debe contemplar siempre un fallo de cache, porque Redis o Memcached pueden desalojar el valor antes de su TTL.
  • Las llamadas directas wp_cache_* permiten grupos de cache propios e invalidación granular.
// Storing complex computed data in a dedicated cache group
$cache_key = 'user_order_summary_' . $user_id;
$order_summary = wp_cache_get($cache_key, 'orders_analytics');

if (false === $order_summary) {
    $order_summary = compute_heavy_user_order_metrics($user_id);
    // Cache for 15 minutes in the orders_analytics group
    wp_cache_set($cache_key, $order_summary, 'orders_analytics', 900);
}

#Grupos de cache y grupos no persistentes

Por defecto, un object cache externo persiste todos los grupos entre peticiones HTTP. Sin embargo, los datos muy volátiles o propios de una sola petición no deberían ocupar la RAM externa ni añadir latencia de ida y vuelta. El núcleo de WordPress ofrece wp_cache_add_non_persistent_groups() para mantener ciertos grupos solo en la memoria del proceso PHP:

// Register non-persistent groups in an early hook or mu-plugin
function register_custom_cache_groups() {
    // Keep temporary session flags in PHP process RAM only
    wp_cache_add_non_persistent_groups([
        'runtime_request_flags',
        'transient_session_tokens',
    ]);
}
add_action('init', 'register_custom_cache_groups');

Para grupos persistentes en redes multisitio, wp_cache_add_global_groups() hace que las claves se compartan entre los sitios de la red en lugar de recibir prefijos por sitio.

#Implementación de object cache en WordPress

Configuración básica de Redis (wp-config.php):

// Enable object cache
define('WP_CACHE', true);

// Redis settings
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_READ_TIMEOUT', 1);

// Advanced settings
define('WP_REDIS_DISABLE_METRICS', false);
define('WP_REDIS_DISABLE_BANNERS', true);
define('WP_REDIS_PREFIX', 'wp_');

Estrategia de micro-cache: Incluso entradas de cache de corta duración aportan beneficios claros:

// Cache recent comments for 30 seconds
function get_cached_recent_comments($limit = 5) {
    $cache_key = 'recent_comments_' . $limit;
    $comments = wp_cache_get($cache_key);

    if (false === $comments) {
        $comments = get_comments([
            'number' => $limit,
            'status' => 'approve',
        ]);
        wp_cache_set($cache_key, $comments, '', 30); // 30 seconds
    }

    return $comments;
}

Object cache relacional: Las implementaciones modernas entienden las relaciones entre objetos:

// Automatic relationship caching
class RelationalObjectCache {
    public function get_post_with_meta($post_id) {
        $cache_key = "post_with_meta:{$post_id}";
        $data = wp_cache_get($cache_key);

        if (false === $data) {
            $post = get_post($post_id);
            $meta = get_post_meta($post_id);
            $author = get_userdata($post->post_author);

            $data = [
                'post' => $post,
                'meta' => $meta,
                'author' => $author,
            ];

            // Cache with relationship tags
            wp_cache_set($cache_key, $data, '', 3600);
            wp_cache_add_relationship($cache_key, "post:{$post_id}");
            wp_cache_add_relationship($cache_key, "user:{$post->post_author}");
        }

        return $data;
    }
}

Para sitios con muchas consultas a la base de datos, como tiendas WooCommerce con miles de productos, el object cache persistente es donde más se nota la diferencia.

#Monitorización y optimización de Redis

Monitorización de la tasa de aciertos:

# Redis CLI commands for monitoring
redis-cli info stats | grep keyspace
redis-cli info memory
redis-cli slowlog get 10

Integración con el admin de WordPress:

// Add cache stats to admin bar
function add_cache_stats_to_admin_bar($wp_admin_bar) {
    $stats = wp_cache_get_stats();
    $hit_rate = ($stats['hits'] / ($stats['hits'] + $stats['misses'])) * 100;

    $wp_admin_bar->add_node([
        'id' => 'cache_stats',
        'title' => sprintf('Cache: %.1f%% hit rate', $hit_rate),
        'href' => admin_url('tools.php?page=redis-cache'),
    ]);
}

#Cómo cachear contenido dinámico en WordPress con ESI

¿Cómo se cachea una página que dice “Hola, [Nombre del Usuario]” o muestra el total de un carrito de compras? En 2026, usamos Edge Side Includes (ESI) o hidratación del lado del cliente para combinar contenido estático y dinámico.

#Por qué el cache de página completa rompe la personalización

El cache de página completa tradicional rompe la personalización:

  • Los saludos al usuario se vuelven estáticos (“Hola, Invitado” para todos)
  • Los carritos de compras muestran estados obsoletos o vacíos
  • Las barras de administrador aparecen para todos los usuarios
  • Los precios dinámicos no se actualizan

#Cómo el fragment caching rellena los marcadores en el edge

La página principal se cachea al 100% como HTML estático. Los elementos dinámicos son marcadores de posición rellenados en el edge o vía JavaScript:

Implementación ESI:

<!-- Static cached content -->
<h1>Welcome to Our Store</h1>

<!-- Dynamic fragment -->
<esi:include src="/esi/cart-count.php" />

<!-- Static content continues -->
<p>Check out our latest products...</p>

<!-- Personalized greeting -->
<esi:include src="/esi/user-greeting.php" />

Hidratación del lado del cliente (alternativa):

<!-- Static placeholder -->
<div id="cart-count" data-dynamic-url="/api/cart-count">
  <span class="placeholder">--</span>
</div>

<script>
// Hydrate on page load
document.addEventListener('DOMContentLoaded', () => {
  document.querySelectorAll('[data-dynamic-url]').forEach(el => {
    fetch(el.dataset.dynamicUrl)
      .then(r => r.text())
      .then(html => el.innerHTML = html);
  });
});
</script>

La elección entre ESI e hidratación del lado del cliente depende de tu arquitectura. ESI es ideal cuando tienes un proxy inverso (Varnish, Nginx) delante de WordPress, mientras que la hidratación del lado del cliente funciona en cualquier entorno y encaja especialmente en sitios que ya utilizan una arquitectura JavaScript moderna.

#Implementación de fragment caching en WordPress

ESI de Varnish con WordPress:

// Mark dynamic content with ESI tags
function esi_dynamic_fragment($content, $callback, $args = []) {
    $esi_url = add_query_arg([
        'esi_action' => $callback,
        'esi_args' => base64_encode(json_encode($args)),
    ], site_url('/esi-endpoint/'));

    return "<esi:include src=\"{$esi_url}\" />";
}

// Usage in templates
echo esi_dynamic_fragment('user_greeting', ['user_id' => get_current_user_id()]);

Exclusión de fragmentos de carrito y checkout en WooCommerce:

En las tiendas WooCommerce estándar, wc-ajax=get_refreshed_fragments se dispara en cada carga de página no cacheada para actualizar el total del carrito y el número de artículos. Cada una de esas peticiones ejecuta PHP en el origen. Retirar el script de fragmentos en las páginas que no muestran carrito, y sustituirlo por ESI o almacenamiento en el cliente donde sí se muestra, elimina esa petición:

// Dequeue cart fragments on non-shop and static content pages
function bypass_woocommerce_cart_fragments() {
    if (is_front_page() || is_single() || is_page()) {
        if (!is_woocommerce() && !is_cart() && !is_checkout()) {
            wp_dequeue_script('wc-cart-fragments');
        }
    }
}
add_action('wp_enqueue_scripts', 'bypass_woocommerce_cart_fragments', 20);

En las páginas de checkout, carrito y cuenta, el cache de página debe omitirse por completo tanto en el edge como en Nginx mediante la inspección de cookies:

  • Cuando existe la cookie woocommerce_items_in_cart, el cache edge omite la búsqueda de página completa y pasa la petición directamente al origen.
  • Las sesiones sensibles (woocommerce_cart_hash, wp_woocommerce_session_) provocan cabeceras de respuesta Cache-Control: private, no-cache, no-store, must-revalidate.
  • Los fragmentos de carrito dentro de páginas de catálogo cacheadas se actualizan desde localStorage y sessionStorage del navegador, refrescando el minicarrito sin lanzar procesos PHP en el origen.

Los fragmentos de carrito y las exclusiones del checkout forman parte del trabajo de latencia de la tienda, además del cableado ESI: ve la guía de rendimiento de WooCommerce 2026.

#Cómo usar la Speculation Rules API para prefetch en WordPress

En 2026, el cache también anticipa lo que pasará, además de guardar lo que ya pasó. Usando la Speculation Rules API, tu sitio WordPress puede predecir y precargar contenido antes de que los usuarios hagan clic.

#Cómo funciona el prefetch especulativo

El ajuste eagerness decide cuándo actúa el navegador. Según la documentación de Chrome sobre prerenderizado, en escritorio eager especula cuando el puntero se detiene sobre un enlace 10 milisegundos, moderate tras 200 milisegundos (o al pulsar, si ocurre antes) y conservative al pulsar con el puntero o el dedo. Con prerender, el navegador renderiza la página destino en segundo plano, así que el siguiente clic se siente instantáneo.

WordPress 6.8 incluye esto en el núcleo. La nota para desarrolladores sobre carga especulativa explica que el valor por defecto “en la práctica da un mode de prefetch y una eagerness de conservative” (traducción propia), que se desactiva cuando hay un usuario con sesión iniciada o cuando el sitio no usa enlaces permanentes bonitos, y que las URL con parámetros de consulta quedan excluidas. El filtro wp_speculation_rules_configuration cambia el modo y la eagerness, y wp_speculation_rules_href_exclude_paths añade exclusiones como las rutas de carrito y checkout.

Implementación:

<script type="speculationrules">
{
  "prerender": [{
    "source": "list",
    "urls": [
      "/about/",
      "/services/",
      "/contact/"
    ]
  }, {
    "source": "document",
    "where": {
      "href_matches": "/blog/*",
      "selector_matches": "a[rel='prerender']"
    }
  }]
}
</script>

#Cómo añadir speculation rules en WordPress con PHP

Prefetch basado en el contenido:

// Add speculation rules based on current page
function add_speculation_rules() {
    if (!is_singular('post')) {
        return;
    }

    // Get related posts for prefetching
    $related = get_related_posts(get_the_ID(), 3);
    $urls = array_map(function($post) {
        return get_permalink($post->ID);
    }, $related);

    // Add next/prev posts
    if ($next = get_next_post()) {
        $urls[] = get_permalink($next->ID);
    }

    $rules = [
        'prerender' => [[
            'source' => 'list',
            'urls' => $urls,
        ]],
    ];

    echo '<script type="speculationrules">' .
         json_encode($rules) .
         '</script>';
}
add_action('wp_head', 'add_speculation_rules');

Prefetch al pasar el cursor (alternativa):

// Prefetch on link hover
let prefetchTimeout;
document.addEventListener('mouseover', (e) => {
  const link = e.target.closest('a');
  if (!link || link.hostname !== location.hostname) return;

  prefetchTimeout = setTimeout(() => {
    const prefetchLink = document.createElement('link');
    prefetchLink.rel = 'prefetch';
    prefetchLink.href = link.href;
    document.head.appendChild(prefetchLink);
  }, 100);
});

document.addEventListener('mouseout', () => {
  clearTimeout(prefetchTimeout);
});

Esta técnica funciona especialmente bien en blogs y sitios de contenido donde los patrones de navegación son predecibles. Un lector que llega a un artículo probablemente navegará al siguiente artículo o a uno relacionado. Pre-renderizar estas páginas elimina el tiempo de carga percibido.

#Cabeceras stale-while-revalidate para WordPress

Los usuarios odian los spinners de carga. SWR permite al navegador servir contenido “obsoleto” (ligeramente antiguo) inmediatamente mientras obtiene la versión fresca en segundo plano.

#Cómo funciona SWR

  1. El usuario solicita una página
  2. El navegador verifica el cache
  3. Si existe contenido cacheado (incluso si expiró), se sirve inmediatamente
  4. En segundo plano, se obtiene contenido fresco del origen
  5. Se actualiza el cache y opcionalmente se refresca el contenido de la página

Cabecera Cache-Control:

Cache-Control: max-age=60, stale-while-revalidate=300

Esto significa:

  • El contenido se considera fresco durante 60 segundos
  • Después de 60 segundos, sirve contenido obsoleto durante hasta 300 segundos mientras revalida
  • Después de 360 segundos en total, requiere contenido fresco

#Cómo implementar SWR en WordPress

Implementación con Service Worker:

// service-worker.js
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // Return cached version immediately (even if stale)
      const fetchPromise = fetch(event.request).then((networkResponse) => {
        caches.open('dynamic').then((cache) => {
          cache.put(event.request, networkResponse.clone());
        });
        return networkResponse;
      });

      // Return stale or wait for fresh
      return response || fetchPromise;
    })
  );
});

Configuración SWR en Cloudflare:

// Add SWR headers to dynamic content
function add_swr_headers($headers) {
    if (is_singular() && !is_user_logged_in()) {
        $headers['Cache-Control'] = 'public, max-age=60, stale-while-revalidate=300';
    }
    return $headers;
}
add_filter('wp_headers', 'add_swr_headers');

SWR es especialmente valioso para sitios de noticias y blogs donde el contenido se actualiza con frecuencia pero los usuarios esperan una carga instantánea.

#Qué directivas Cache-Control necesitan las páginas WordPress en el edge

Stale-while-revalidate es una directiva entre varias, y las demás deciden si una página puede cachearse o no. La referencia de Cache-Control de MDN define las que importan para WordPress:

DirectivaDefinición de MDN (resumen)Uso en WordPress
s-maxageFrescura en un cache compartido; ahí prevalece sobre max-age y ExpiresTTL largo en el edge para HTML anónimo, con un max-age corto en el navegador
stale-while-revalidateEl cache puede reutilizar una respuesta obsoleta mientras la revalidaArchivos y artículos que cambian unas pocas veces al día
stale-if-errorEl cache puede reutilizar una respuesta obsoleta cuando el origen devuelve 500, 502, 503 o 504Mantiene las páginas en línea durante una caída de PHP-FPM o de la base de datos
privateSolo un cache privado, como el del navegador, puede guardar la respuestaPáginas de cuenta, vistas con sesión iniciada
no-storeNingún cache de ningún tipo puede guardar la respuestaCarrito, checkout, restablecimiento de contraseña

Dos detalles de la documentación de Cloudflare sobre cache-control cambian cómo se combinan. Primero, Cloudflare trata s-maxage como una regla según la cual un cache compartido debe revalidar con el origen antes de servir contenido obsoleto, así que s-maxage desactiva stale-while-revalidate en su red. Si quieres revalidación en segundo plano en el edge de Cloudflare, envía max-age con stale-while-revalidate y controla el TTL del edge con una Cache Rule. Segundo, con Origin Cache Control activado y el nivel de cache por defecto, una respuesta que lleva Set-Cookie no se cachea. WordPress pone cookies a los usuarios con sesión y WooCommerce a los carritos, así que un Set-Cookie perdido en una página pública desactiva sin avisar tu cache edge.

El punto de partida también cuenta. La página de comportamiento por defecto de Cloudflare afirma: “La CDN de Cloudflare no cachea HTML ni JSON por defecto” (traducción propia). Cachea según la extensión del archivo, no según el tipo MIME. Por eso el cache de página completa en el edge para WordPress siempre necesita una Cache Rule explícita que marque el HTML como cacheable, con condiciones de exclusión para wp-admin, wp-login.php, la REST API cuando devuelve datos privados y las cookies wordpress_logged_in_ y woocommerce_items_in_cart.

#Cómo monitorear y depurar el cache de WordPress

Un cache efectivo requiere visibilidad sobre lo que está sucediendo. En 2026, usamos herramientas de monitoreo para comprobar la efectividad del cache.

#Cómo añadir cabeceras de depuración X-Cache-Status

Agrega cabeceras de depuración para rastrear el comportamiento del cache:

function add_cache_debug_headers() {
    if (!current_user_can('manage_options')) {
        return;
    }

    header('X-Cache-Status: ' . (did_cache_hit() ? 'HIT' : 'MISS'));
    header('X-Cache-TTL: ' . get_cache_remaining_ttl());
    header('X-Cache-Tags: ' . implode(', ', get_current_page_cache_tags()));
}
add_action('template_redirect', 'add_cache_debug_headers');

#Cómo registrar aciertos y fallos de cache en Redis TimeSeries

// Log cache metrics to Redis TimeSeries
function log_cache_metric($metric, $value) {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);

    // Add to time series
    $redis->rawCommand('TS.ADD',
        "cache:{$metric}",
        '*',
        $value,
        'LABELS', 'site', get_site_url(), 'type', 'wordpress'
    );
}

// Log on every request
add_action('wp', function() {
    log_cache_metric('requests', 1);
    if (did_cache_hit()) {
        log_cache_metric('hits', 1);
    } else {
        log_cache_metric('misses', 1);
    }
});

#Panel de monitorización del cache en el admin de WordPress

Crea un panel en el admin de WordPress para monitorizar el cache:

// Add cache health page
function add_cache_health_page() {
    add_management_page(
        'Cache Health',
        'Cache Health',
        'manage_options',
        'cache-health',
        'render_cache_health_page'
    );
}
add_action('admin_menu', 'add_cache_health_page');

function render_cache_health_page() {
    $stats = wp_cache_get_stats();
    $hit_rate = $stats['hits'] / ($stats['hits'] + $stats['misses']) * 100;
    ?>
    <div class="wrap">
        <h1>Cache Health Dashboard</h1>
        <div class="cache-stats">
            <div class="stat-box">
                <h3>Hit Rate</h3>
                <div class="stat-value <?php echo $hit_rate > 90 ? 'good' : 'warning'; ?>">
                    <?php echo number_format($hit_rate, 1); ?>%
                </div>
            </div>
            <div class="stat-box">
                <h3>Cached Objects</h3>
                <div class="stat-value"><?php echo number_format($stats['objects']); ?></div>
            </div>
            <div class="stat-box">
                <h3>Memory Used</h3>
                <div class="stat-value"><?php echo size_format($stats['memory']); ?></div>
            </div>
        </div>
    </div>
    <?php
}

El monitoreo continuo del cache es tan importante como su implementación. Sin visibilidad, es imposible saber si tu estrategia de cache está funcionando correctamente. Una caída brusca de la tasa de aciertos suele indicar un problema de configuración o un patrón de tráfico inesperado.

#Cómo detecta Site Health de WordPress el cache de página y el object cache

Desde WordPress 6.1, Herramientas, Salud del sitio ejecuta dos pruebas que comprueban el cache desde dentro, y sus reglas son públicas en el núcleo. Leerlas explica por qué un sitio con una CDN que funciona puede seguir mostrando un aviso.

La prueba de cache de página hace tres peticiones a la página de inicio, registra las cabeceras de respuesta y toma la mediana del tiempo de respuesta. Una respuesta cuenta como cacheada cuando lleva alguna de las cabeceras listadas en get_page_cache_headers(): cache-control con un max-age distinto de cero, una fecha expires en el futuro, un age positivo, last-modified, etag o via, además de cabeceras de proveedores como cf-cache-status, x-cache, x-cache-status, x-litespeed-cache, x-srcache-fetch-status con valor HIT, o x-varnish con dos identificadores de petición. Un plugin de cache de página solo se detecta cuando se cumplen tres condiciones en el código del núcleo: existe wp-content/advanced-cache.php, la constante WP_CACHE es verdadera y el filtro enable_loading_advanced_cache_dropin no se ha desactivado. Esas mismas tres condiciones deciden si wp-settings.php carga advanced-cache.php, muy pronto, antes que los plugins.

El umbral de tiempo de respuesta es de 600 milisegundos por defecto. La función get_good_response_time_threshold() del núcleo documenta que el valor se basa en la guía de web.dev sobre el time to first byte, y el filtro site_status_good_response_time_threshold lo cambia. Un sitio detrás de Cloudflare que responde en 900 ms desde el origen, porque la prueba no pasa por el edge o el HTML no está cacheado en él, recibirá el aviso aunque los visitantes obtengan respuestas rápidas.

La prueba de object cache persistente recomienda Redis o Memcached cuando se cumple cualquiera de estas condiciones:

  • el sitio es una red multisitio (siempre se sugiere),
  • más de 500 opciones autocargadas, o más de 100.000 bytes de opciones autocargadas serializadas,
  • 1.000 filas o más en las tablas de comentarios, opciones, posts, términos o usuarios (el núcleo lee estimaciones de filas de InnoDB en information_schema, no recuentos exactos).

Los umbrales se pueden cambiar con el filtro site_status_persistent_object_cache_thresholds, y site_status_should_suggest_persistent_object_cache anula la comprobación entera. Los hostings que ofrecen object cache en otra capa usan ese filtro, así que la ausencia del aviso no prueba que haya un drop-in instalado: comprueba si existe wp-content/object-cache.php o llame a wp_using_ext_object_cache().

#Errores comunes de cache en WordPress y soluciones

#Cómo evitar un cache demasiado agresivo

Problema: cachear todo conduce a contenido obsoleto y frustración del usuario.

Solución: implementar exclusiones de cache:

function should_cache_page() {
    // Don't cache for logged-in users
    if (is_user_logged_in()) {
        return false;
    }

    // Don't cache dynamic forms
    if (is_page('contact') || is_page('checkout')) {
        return false;
    }

    // Don't cache search results (too many variations)
    if (is_search()) {
        return false;
    }

    return true;
}

#Cómo prevenir un cache stampede

Problema: cuando expira un elemento de cache popular, cientos de peticiones concurrentes encuentran un fallo de cache a la vez. Cada proceso lanza la misma consulta SQL pesada o la misma llamada a una API remota contra el origen, lo que provoca picos de CPU, contención de bloqueos y caídas del servidor (dogpiling o thundering herd).

Solución: implementar expiración anticipada probabilística con el algoritmo XFetch o bloqueos mutex distribuidos en Redis:

#Expiración anticipada probabilística con el algoritmo XFetch

Este enfoque evita por completo los bloqueos rígidos. Propuesto por Vattani, Chierichetti y Lowenstein, el algoritmo XFetch calcula una probabilidad de refresco anticipado que crece a medida que el elemento se acerca a su expiración y a medida que aumenta el tiempo de cálculo. Un worker refresca el elemento antes de tiempo mientras sigue sirviendo datos cacheados válidos a todas las peticiones entrantes:

/**
 * Probabilistic early cache recomputation (XFetch algorithm).
 *
 * @param string   $key      Cache identifier
 * @param callable $compute  Callback generating fresh data
 * @param int      $ttl      Cache TTL in seconds
 * @param float    $beta     Aggressiveness multiplier (> 0, default 1.0)
 * @return mixed             Cached or computed payload
 */
function xfetch_cache_get($key, callable $compute, $ttl = 3600, $beta = 1.0) {
    $cached = wp_cache_get($key, 'xfetch_group');

    // Expected format: ['data' => ..., 'delta' => compute_time_seconds, 'expiry' => timestamp]
    if (is_array($cached) && isset($cached['data'], $cached['delta'], $cached['expiry'])) {
        $remaining = $cached['expiry'] - time();
        // Compute probabilistic trigger: -beta * delta * ln(random_float(0, 1))
        $random = mt_rand(1, 1000000) / 1000000;
        $early_trigger = -($beta * $cached['delta'] * log($random));

        // If remaining lifetime exceeds early trigger threshold, return cached value immediately
        if ($remaining > $early_trigger) {
            return $cached['data'];
        }
    }

    // Refresh cache item
    $start = microtime(true);
    $fresh_data = $compute();
    $delta = microtime(true) - $start;

    $payload = [
        'data'   => $fresh_data,
        'delta'  => max($delta, 0.001),
        'expiry' => time() + $ttl,
    ];

    wp_cache_set($key, $payload, 'xfetch_group', $ttl * 2);
    return $fresh_data;
}

#Bloqueo mutex distribuido con Redis

Cuando el precálculo en segundo plano no encaja, un bloqueo mutex distribuido garantiza que exactamente un proceso PHP-FPM recalcule la consulta costosa:

function get_cached_with_distributed_lock($key, callable $compute, $ttl = 3600) {
    $data = wp_cache_get($key, 'distributed_cache');
    if (false !== $data) {
        return $data;
    }

    $lock_key = 'lock:' . $key;
    // Atomic SET lock:key token NX EX 10
    $lock_acquired = wp_cache_add($lock_key, 1, 'distributed_locks', 10);

    if ($lock_acquired) {
        try {
            $fresh_data = $compute();
            wp_cache_set($key, $fresh_data, 'distributed_cache', $ttl);
            return $fresh_data;
        } finally {
            wp_cache_delete($lock_key, 'distributed_locks');
        }
    }

    // If another process holds the lock, wait briefly or serve fallback data
    usleep(80000); // 80ms sleep
    $data = wp_cache_get($key, 'distributed_cache');
    return (false !== $data) ? $data : $compute();
}

#Cómo variar el cache para dispositivos móviles

Problema: los usuarios de escritorio y móvil reciben la misma versión cacheada, lo que rompe el diseño responsivo.

Solución: variar el cache por tipo de dispositivo:

// Add device type to cache key
function get_device_cache_key($base_key) {
    $device = wp_is_mobile() ? 'mobile' : 'desktop';
    return "{$base_key}_{$device}";
}

// Or use Vary header for CDN
function add_device_vary_header($headers) {
    $headers['Vary'] = 'User-Agent';
    return $headers;
}

#Conclusiones

El cache ya no se trata de ahorrar espacio, sino de ahorrar tiempo. En 2026, una estrategia de cache inteligente es la diferencia entre un sitio WordPress de nivel empresarial y un competidor rezagado.

Al dominar edge caching, cache tags, fragment caching y patrones SWR, tu sitio WordPress pasa a funcionar como un sistema distribuido de alto rendimiento capaz de atender millones de solicitudes.

Puntos clave:

  1. Implementa cache edge de triple capa para rendimiento global
  2. Usa cache tagging para invalidación quirúrgica
  3. Despliega Redis 8+ para object caching empresarial
  4. Combina contenido estático y dinámico con fragment caching
  5. Adopta SWR para experiencias de usuario instantáneas
  6. Monitorea la salud del cache continuamente
  7. Prueba exhaustivamente: los bugs cacheados son más difíciles de encontrar

¿Tu cache está trabajando a tu favor o en tu contra? Contacta con WPPoland para auditar y mejorar tu arquitectura de cache.


#Recursos relacionados

Siguiente paso

Transforma el artículo en una implementación real

Este bloque refuerza el enlazado interno y lleva al lector al siguiente paso más útil dentro de la arquitectura del sitio.

¿Quieres implementar esto en tu sitio?

Si el problema está en los Core Web Vitals, en el rendering lento o en el peso de WordPress, puedo mapear e implementar la optimización.

Preguntas frecuentes

Respuestas prácticas para aplicar el tema en la ejecución real.

¿El Edge Caching hace obsoletos plugins como WP Rocket?#
No del todo. Mientras el Edge maneja la entrega, los plugins de cache locales en 2026 se enfocan en optimización de archivos, generación de CSS crítico y ajuste específico de la base de datos local que los servicios edge no pueden realizar.
¿Cómo manejamos usuarios con sesión iniciada con cache?#
Usando Cache Varying vía cookies o service workers, asegurando que los usuarios con sesión obtengan una experiencia personalizada mientras se benefician de la velocidad del object cache para consultas de base de datos.
¿Cuál es el mayor riesgo del cache agresivo?#
Contenido obsoleto. En 2026, mitigamos esto con patrones Stale-While-Revalidate (SWR) y hooks de purga instantánea vía API que limpian contenido específico sin afectar todo el cache.
¿Redis es mejor que Memcached para WordPress?#
Para la mayoría de sitios WordPress, Redis es la opción más flexible: ofrece más tipos de datos, persistencia opcional RDB o AOF y replicación, mientras que Memcached es un almacén clave-valor en memoria con desalojo LRU y sin replicación. Ninguno de los dos ofrece cache tags nativos.
¿Cuánto puede mejorar la velocidad del sitio el cache avanzado?#
Depende de dónde se va hoy el tiempo. Mide primero: Site Health de WordPress considera buena una mediana de respuesta de la página de inicio por debajo de 600 ms, y el HTML cacheado en el edge evita por completo PHP y la base de datos, así que la mejora es mayor en sitios donde dominan el HTML sin cachear y las consultas lentas.

¿Necesitas un FAQ adaptado a tu sector y mercado? Preparamos una versión alineada con tus objetivos de negocio.

Hablemos

Artículos Relacionados