Caché HTTP: caché fuerte, negociado y estrategias de CDN
Caché HTTP: la primera línea de defensa de la optimización del rendimiento
Una configuración de caché adecuada puede lograr cero solicitudes de red en el segundo acceso, reduciendo el tiempo de carga de segundos a milisegundos.
| Tipo de caché | Mecanismo | ¿Solicita al servidor? | Velocidad |
|---|---|---|---|
| Caché fuerte | Cache-Control / Expires | No | La más rápida (0ms) |
| Caché negociado | ETag / Last-Modified | Sí (petición condicional) | Rápida (respuesta 304) |
| Sin caché | no-cache / no-store | Sí (petición completa) | La más lenta |
I. Caché fuerte
Directivas Cache-Control
Cache-Control: max-age=31536000, immutable
| Directiva | Función | Escenario aplicable |
|---|---|---|
max-age=N |
Cachear N segundos | Recursos estáticos |
s-maxage=N |
CDN cachea N segundos (sobreescribe max-age) | Escenarios CDN |
immutable |
No revalidar incluso si expira | Nombres de archivo con hash |
public |
Permitir caché intermedio | CDN |
private |
Solo caché del navegador | Contenido personalizado del usuario |
no-cache |
Debe verificar antes de cada uso | Páginas HTML |
no-store |
No cachear nada | Datos sensibles |
must-revalidate |
Debe verificar después de expirar | Respuestas API |
stale-while-revalidate=N |
Devolver valor antiguo tras expirar, actualizar en segundo plano | Actualización progresiva |
Expires (estándar antiguo)
Expires: Thu, 02 Jun 2027 00:00:00 GMT
Prioridad: Cache-Control: max-age > Expires.
II. Caché negociado
ETag / If-None-Match
# Primera petición
GET /api/data
→ 200 OK
ETag: "abc123"
Cache-Control: max-age=0
# Petición siguiente (condicional)
GET /api/data
If-None-Match: "abc123"
→ 304 Not Modified (sin cuerpo de respuesta, ahorra ancho de banda)
Last-Modified / If-Modified-Since
# Primera petición
GET /style.css
→ 200 OK
Last-Modified: Mon, 01 Jun 2026 12:00:00 GMT
# Petición siguiente
GET /style.css
If-Modified-Since: Mon, 01 Jun 2026 12:00:00 GMT
→ 304 Not Modified
ETag vs Last-Modified
| Dimensión | ETag | Last-Modified |
|---|---|---|
| Precisión | Nivel de byte | Nivel de segundo |
| Rendimiento | Requiere cálculo de hash | Lectura directa del tiempo del archivo |
| Fiabilidad | Alta | Baja (múltiples modificaciones en 1 segundo se pierden) |
| Distribuido | Debe garantizar hash consistente | Naturalmente consistente |
Recomendación: Priorizar ETag.
III. Matriz de estrategias de caché
Páginas HTML
Cache-Control: no-cache
HTML es el archivo de entrada, debe verificarse cada vez para asegurar que los usuarios obtengan la última versión.
Recursos estáticos con Hash
Cache-Control: max-age=31536000, immutable
El nombre del archivo contiene Hash (ej., app.abc123.js), cuando el contenido cambia, el Hash cambia → la URL cambia → nueva petición. La URL antigua nunca cambia, se puede cachear un año y marcar como immutable.
Recursos estáticos sin Hash
Cache-Control: max-age=86400, stale-while-revalidate=604800
Tras expirar, primero devuelve el valor antiguo, actualiza silenciosamente en segundo plano, imperceptible para el usuario.
Respuestas API
Cache-Control: private, max-age=0, must-revalidate
Las respuestas API suelen contener datos de usuario, private prohíbe caché CDN, must-revalidate asegura que no se devuelvan valores antiguos tras expirar.
IV. Cabecera Vary: extensión de la clave de caché
Vary: Accept-Encoding, Accept-Language
Vary indica a la caché: además de la URL, debe distinguir versiones de caché según los valores de estas cabeceras de petición.
| Valor Vary | Significado | Escenario |
|---|---|---|
Accept-Encoding |
Cachear por método de compresión | gzip vs br |
Accept-Language |
Cachear por idioma | Sitios multilingües |
Cookie |
Cachear por Cookie | Páginas personalizadas |
* |
Cada petición con caché independiente | Equivale a no cachear |
Nota: Vary: Cookie causa una tasa de acierto de caché muy baja, usar con precaución.
V. Estrategia de caché CDN
Control de recuperación de origen
# CDN cachea 1 día, navegador cachea 5 minutos
Cache-Control: public, s-maxage=86400, max-age=300
| Escenario | s-maxage | max-age | Descripción |
|---|---|---|---|
| Recursos estáticos | 31536000 | 31536000 | CDN y navegador con caché larga |
| Respuestas API | 300 | 0 | CDN cachea 5 minutos, navegador no cachea |
| HTML | 60 | 0 | CDN cachea 1 minuto, navegador no cachea |
| Contenido personalizado | — | 0 | CDN no cachea, navegador no cachea |
Refresco de caché CDN
| Método | Velocidad | Aplicable |
|---|---|---|
| Precalentamiento de URL | Rápido | Nuevo contenido publicado |
| Refresco completo | Lento | Corrección urgente |
| Refresco de directorio | Medio | Actualización por lotes |
| Cache-Control: no-cache | Automático | Escenarios API |
Stale-While-Revalidate en la práctica
Cache-Control: max-age=60, stale-while-revalidate=86400
Segundo 0: Petición → Caché MISS → Recuperar origen → Devolver valor fresco
Segundo 30: Petición → Caché HIT → Devolver valor en caché
Segundo 90: Petición → Caché STALE → Devolver valor antiguo inmediatamente + recuperar origen en segundo plano
Segundo 91: Recuperación de origen completada → Caché actualizada
Segundo 120: Petición → Caché HIT → Devolver valor nuevo
Efecto: El usuario nunca espera la recuperación de origen, actualización silenciosa en segundo plano.
VI. Estrategia de invalidación de caché
Hash de contenido (recomendado)
<!-- Hash generado en tiempo de compilación -->
<script src="/app.abc123.js"></script>
<link href="/style.def456.css">
// Configuración webpack
output: {
filename: '[name].[contenthash:8].js',
}
Parámetro de consulta de versión
<!-- Simple pero CDN puede no cachear parámetros ?v= -->
<script src="/app.js?v=2.0.0"></script>
Problema: Algunos CDN ignoran los parámetros de consulta, causando que la caché no se actualice.
Marca de tiempo (no recomendado)
<!-- Cambia cada vez, destruye completamente la caché -->
<script src="/app.js?t=1717315200"></script>
VII. Caché de Service Worker
Estrategia Cache First
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
);
});
Estrategia Stale While Revalidate
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('dynamic').then(cache =>
cache.match(event.request).then(cached => {
const fetchPromise = fetch(event.request)
.then(response => {
cache.put(event.request, response.clone());
return response;
});
return cached || fetchPromise;
})
)
);
});
Selección de estrategia de caché
| Estrategia | Origen prioritario | Aplicable |
|---|---|---|
| Cache First | Caché | Fuentes, imágenes |
| Network First | Red | API, HTML |
| Stale While Revalidate | Caché + actualización en segundo plano | CSS, JS |
| Network Only | Red | Peticiones no GET |
| Cache Only | Caché | Páginas offline |
VIII. Depuración de caché
Chrome DevTools
- DevTools → Panel Network
- Ver columna
Size:from disk cache: Acierto en caché de discofrom memory cache: Acierto en caché de memoria- Tamaño real: Sin acierto
- Ver cabecera de respuesta
Age: Tiempo que lleva en caché - Marcar Disable cache: Deshabilitar caché (cuando DevTools está abierto)
Verificación con curl
# Ver cabeceras de respuesta
curl -I https://example.com/style.css
# Enviar petición condicional
curl -I -H 'If-None-Match: "abc123"' https://example.com/style.css
# 304 indica caché negociado acertado
# Simular diferente Accept-Encoding
curl -I -H 'Accept-Encoding: br' https://example.com/style.css
IX. Lista de mejores prácticas
- HTML: no-cache, asegurar que la entrada siempre es la más reciente
- JS/CSS con Hash: max-age=1 año + immutable
- API: must-revalidate, debe verificar tras expirar
- CDN: s-maxage para controlar la frecuencia de recuperación de origen
- Vary: Accept-Encoding, distinguir métodos de compresión
- Usar ETag en lugar de Last-Modified
- stale-while-revalidate para actualización imperceptible
- Service Worker para complementar capacidades offline