Saltar al contenido

Caché en Next.js: las cuatro capas y cuál te ha mordido

La memoización de peticiones, el Data Cache, el Full Route Cache y el Router Cache son cuatro mecanismos con cuatro vidas distintas. Casi todo fallo de caché es confundirlos.

7 min de lectura

Casi todas las quejas sobre caché que nos llegan se reducen a la misma frase: "cambiamos los datos y la página no se actualizó". Es una descripción razonable del síntoma y una descripción inútil de la causa, porque Next.js tiene cuatro cachés y la solución es distinta en cada una.

Vale la pena separarlas: tienen claves distintas, vidas distintas y formas distintas de invalidarse.

Las cuatro capas

CapaViveÁmbitoSe limpia con
Memoización de peticionesUn pase de renderServidorNada - expira sola
Data CacheEntre peticiones y desplieguesServidorrevalidateTag, revalidatePath, tiempo
Full Route CacheHasta revalidar o redesplegarServidorLas mismas llamadas, más un build nuevo
Router CacheDe segundos a minutosEl navegador de un visitanteNavegación, router.refresh()

Lee esa tabla dos veces. Tres viven en el servidor y una vive en la pestaña del visitante, y la de la pestaña es la que produce los informes que no tienen sentido: la página es correcta en una ventana de incógnito y está obsoleta para quien abrió el ticket.

Memoización de peticiones

Dentro de un mismo render, dos llamadas fetch idénticas con la misma URL y las mismas opciones se ejecutan una sola vez. Es de React, no de Next.js, y existe para que un layout y una página puedan pedir el usuario actual sin que tengas que pasarlo por props.

// Ambas se ejecutan; solo una petición sale del servidor.
const user = await fetch('https://api.example.com/me').then((r) => r.json());

Su ámbito es un pase de render. No puede quedar obsoleta, no se puede limpiar y casi nunca es la capa que causó tu fallo. Conviene conocerla sobre todo para dejar de escribir código de caché que la duplica.

El Data Cache

Esta es la que de verdad hay que entender. Guarda el resultado de fetch en el servidor, sobrevive a las peticiones y sobrevive a los despliegues.

Esto último sorprende. Un redespliegue no vacía el Data Cache.

// En caché hasta que algo invalide la etiqueta.
const posts = await fetch('https://cms.example.com/posts', {
  next: { tags: ['posts'] },
});
 
// En caché durante 60 segundos como máximo.
const rates = await fetch('https://api.example.com/rates', {
  next: { revalidate: 60 },
});
 
// Nunca en caché.
const cart = await fetch('https://api.example.com/cart', {
  cache: 'no-store',
});

Las etiquetas son el mecanismo que vuelve manejable un sitio de contenido. Etiqueta el fetch y deja que el webhook del CMS lo limpie:

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
 
export async function POST(request: Request) {
  const secret = request.headers.get('x-webhook-secret');
  if (secret !== process.env.REVALIDATE_SECRET) {
    return new Response('Unauthorised', { status: 401 });
  }
 
  revalidateTag('posts');
  return Response.json({ revalidated: true });
}

La comprobación del secreto no es opcional. Un endpoint de revalidación sin autenticar es un botón gratuito de vaciar caché para cualquiera que encuentre la URL, y vaciar la caché de un sitio con tráfico significa que todas las peticiones van al origen a la vez.

El Full Route Cache

Cuando una ruta no tiene entradas dinámicas, Next.js la renderiza en el build y sirve el HTML y el payload RSC guardados. Es la caché a la que la gente se refiere cuando dice "estática".

Lo interesante es lo fácil que resulta salir de ella. Leer cookies(), headers() o searchParams, o llamar a un fetch con cache: 'no-store', lleva la ruta entera al render dinámico. Basta un helper metido en un componente compartido, y la única señal visible es la salida del build:

Route (app)
┌ ○ /                     static
├ ƒ /products             dynamic     <- la semana pasada era estática
└ ○ /about                static

Si una ruta que esperabas estática muestra ƒ, algo por debajo leyó un valor en tiempo de petición. Encontrar qué es cuestión de leer el árbol, no de adivinar - y vale la pena, porque una ruta dinámica te cuesta el CDN.

El Router Cache

Este vive en el navegador del visitante, y es la razón de que tu compañera vea datos viejos después de que le dijeras que estaba arreglado.

Cuando un visitante navega en cliente, el payload RSC del destino se mantiene en memoria durante una ventana corta. Si sale y vuelve dentro de esa ventana, no se vuelve a pedir nada. Una recarga dura la limpia; una navegación blanda no.

Después de una mutación, límpiala explícitamente:

'use client';
 
import { useRouter } from 'next/navigation';
 
export function DeleteButton({ id }: { id: string }) {
  const router = useRouter();
 
  async function remove() {
    await fetch(`/api/items/${id}`, { method: 'DELETE' });
    router.refresh(); // Descarta la caché de cliente y re-renderiza en servidor.
  }
 
  return <button onClick={remove}>Eliminar</button>;
}

En una Server Action, revalidatePath hace las dos mitades - limpia la caché del servidor y marca la del cliente como obsoleta - y por eso las mutaciones van en actions y no en route handlers escritos a mano siempre que puedas elegir.

Cómo lo depuramos en la práctica

La pregunta siempre es "qué capa", y se responde en tres pasos.

  1. ¿Está obsoleto para todos o para una persona? Una persona: es el Router Cache. Todos: está en el servidor.
  2. ¿Lo arregla un redespliegue? Si sí, era el Full Route Cache. Si no, es el Data Cache, que -de nuevo- sobrevive a los despliegues.
  3. ¿La ruta muestra ƒ en la salida del build? Si lo muestra y esperabas , no estás depurando una caché. Estás depurando un render dinámico accidental, que es otro problema y normalmente más caro.

La mayoría de los equipos con los que trabajamos no necesitan una estrategia de caché más agresiva. Necesitan saber con cuál de las cuatro están discutiendo, y una convención de nombres de etiquetas que de verdad apliquen.

Eso, y el modelo de render que hay debajo: el modelo de renderizado del App Router, explicado como toca.

Volver a todos los artículos