Diario / Desarrollo de producto web

Cómo funciona la caché en Next.js 13+ con App Router

Cómo funciona la caché en Next.js 13+ con App Router

Un repaso detallado de cómo funciona la caché en Next.js 13+, qué tipos de caché existen y cómo afectan al rendimiento y al SEO.

Qué es la caché y cómo funciona en Next.js

La caché sirve para guardar el resultado de un cálculo o de una petición y no repetirlos, de modo que la aplicación va más rápido. En Next.js se pueden cachear las páginas HTML, los datos, las peticiones, el renderizado de componentes e incluso rutas enteras.

Con la llegada del App Router (desde Next.js 13) la filosofía cambió: la caché está activada por defecto y es el desarrollador quien decide desactivarla cuando necesita un comportamiento dinámico.

La política de Next.js es esta:

"Cacheamos todo, salvo que se indique lo contrario"

Si no indica de forma explícita cache: 'no-store', next: { revalidate } o dynamic = 'force-dynamic', ocurre lo siguiente:

  • fetch() se guardará en caché
  • el layout y la página también pueden guardarse en caché
  • las rutas se añadirán a la caché de rutas
  • el renderizado de la página pasará a la Full Route Cache

Esto da rendimiento, pero puede provocar datos desactualizados y problemas al depurar.

Tipos de caché en Next.js

Fuente: documentación de Next.js

Full Route Cache (caché de rutas en el servidor)

Se usa con ISR (Incremental Static Regeneration, regeneración estática incremental).

  • Se guarda en caché la página completa (HTML, datos de layout.tsx, page.tsx, fetch(), headers(), cookies()).
  • La caché está en el servidor.
  • Funciona por defecto cuando no hay comportamiento dinámico.
  • Se desactiva con revalidate o revalidateTag().

Es la mejor opción para blogs, páginas de marketing y otros contenidos que cambian poco.

Ejemplo:

export const revalidate = 60; // regenerate the page every 60 seconds

Data Cache (caché de datos en el servidor)

  • Las respuestas de fetch() en Server Components se guardan en caché.
  • Se controla con next: { revalidate } o cache: 'no-store'.

Ejemplo:

await fetch('https://api.com/posts', {
  next: { revalidate: 300 },
});

Request Memoization (caché por petición, en el servidor)

Es una optimización interna de Next.js: el mismo fetch() o la misma función con los mismos argumentos se ejecuta una sola vez por renderizado.

  • Durante un renderizado en el servidor, Next.js hace seguimiento de las peticiones idénticas.
  • Si ya se han ejecutado, el resultado se toma de la caché.
  • Funciona con independencia de cache: 'force-cache' o no-store.

Es útil para:

  • DRY (no repetirse) al obtener los mismos datos en distintos componentes.
  • Mejorar el rendimiento sin optimizar a mano.
  • Cuando el mismo fetch se usa tanto en layout.tsx como en page.tsx.

No sirve para:

  • Reutilizar datos entre peticiones o usuarios distintos (no es una caché global).
  • Saltarse la Data Cache: trabaja sobre la base de la Request Memoization.
// lib/getUser.ts
export async function getUser() {
  return fetch('https://api.com/user').then(res => res.json());
}

// app/layout.tsx
export default async function Layout({ children }) {
  const user = await getUser(); // Call 1
  return (
    <div>
      <Sidebar user={user} />
      {children}
    </div>
  );
}

// app/page.tsx
import { getUser } from '../lib/getUser';

export default async function Page() {
  const user = await getUser(); // Call 2, but actually not repeated
  return <p>Hello, {user.name}</p>;
}

Físicamente, el fetch se ejecutará una sola vez y el resultado se usará en los dos sitios.

Router Cache (caché en el cliente)

Es una caché que usted controla con bibliotecas como SWR, React Query y otras.

  • Solo funciona con la navegación en el cliente (<Link>, router.push()).
  • Next.js no controla esta caché: usted decide cómo recordar los datos.
  • Es muy rápida, porque no se vuelve a renderizar nada: React simplemente monta desde la memoria.
  • Pero solo existe hasta que el usuario recarga la página (F5).

Es la mejor opción para componentes interactivos, formularios y paneles de control.

Ejemplo:

'use client';
import useSWR from 'swr';

const { data } = useSWR('/api/data', fetcher);

Comparación visual de los tipos de caché

Tipo de cachéDónde funcionaControlEjemplo
Full Route CacheServidor (toda la ruta)revalidate, tagexport const revalidate = 60
Data CacheServidor (fetch)cache, revalidatefetch(..., { next: ... })
Caché generada estáticamenteServidor (en la compilación)NingunoJSX sin datos
Caché en el clienteNavegador / clienteUstedSWR / React Query

Un ejemplo del día a día

Supongamos que está construyendo una tienda online:

PáginaQué guardarQué tipo de caché usar
/productosLista de productos, actualización cada 5 minutosFull Route Cache (ISR)
/api/productosDatos de producto de la APIData Cache (con revalidateTag)
Navegación SPANavegación rápidaRouter Cache (caché en el cliente)

Problemas habituales de la caché en App Router

Los datos se han actualizado, pero la página muestra los antiguos

fetch() usa force-cache por defecto si no indica cache: 'no-store' o next: { revalidate }. Como resultado, Next.js recuerda los datos y nunca los actualiza.

Solución:

  • Añada cache: 'no-store' para desactivar la caché
  • O next: { revalidate: 60 } para ISR
fetch('https://api.com/data', { next: { revalidate: 60 } });

La URL (searchParams) ha cambiado, pero los datos son los mismos

Next.js no repite la petición de fetch si la URL es la misma. Si utiliza parámetros, añádalos directamente a la URL del fetch:

fetch(`https://api.com/posts?category=${searchParams.category}`, {
  next: { revalidate: 60 }
});

Se navega con <Link>, pero los datos no se han actualizado

Se ha usado la Router Cache (caché en el cliente). Next.js no envía una petición nueva al volver a la página.

Solución:

  • Fuerce la actualización de los datos con router.refresh()
  • Use cache: 'no-store' si los datos deben estar siempre al día

revalidatePath() o revalidateTag() no funciona

  • No se ha creado la caché (por ejemplo, por no-store)
  • No usa tags en el fetch
  • Llama a revalidatePath() fuera de una Server Action

Solución:

  • Compruebe que el fetch usa next: { tags: [...] }
  • Llame a revalidateTag('tag') en una función del servidor

El layout está en caché y no se actualiza

layout.tsx también se cachea como RSC con revalidate, y si los datos de su interior están en caché, no se actualizarán hasta el siguiente ISR.

Solución:

  • Use cache: 'no-store' en los fetch de layout.tsx si contiene, por ejemplo, el perfil del usuario
  • O mueva el fetch del layout a la página (o a un componente cliente con SWR)

Comportamiento inesperado en modo desarrollo (dev)

En next dev muchas cachés están desactivadas o funcionan de otra manera, y revalidate no funciona de forma fiable en modo desarrollo.

Consejo:

Pruebe la caché con next build && next start o en una vista previa de Vercel.

La combinación de cookies(), headers(), params y fetch() rompe la caché

Cuando utiliza elementos dinámicos como:

cookies()

headers()

useSearchParams()

generateMetadata() con valores dinámicos

Next.js desactiva automáticamente la caché de la página y pasa a dynamic = 'force-dynamic'. Lista de comprobación: cómo evitar problemas con la caché

Defina siempre de forma explícita el comportamiento del fetch:

  • No olvide incluir los parámetros de consulta directamente en la URL del fetch
  // no cache at all
  fetch(..., { cache: 'no-store' })

  // ISR (revalidate every 60 seconds)
  fetch(..., { next: { revalidate: 60 } })

  // manual invalidation via tag
  fetch(..., { next: { tags: ['products'] } })
  • No olvide poner los parámetros de consulta directamente en la URL que pasa a fetch
  • Pruebe con next build && next start; de lo contrario puede tener una falsa sensación de seguridad
  • Si necesita invalidar los datos a mano, use revalidatePath() o revalidateTag()

Si la caché de su proyecto con App Router ya está en producción y da problemas, ese es el tipo de trabajo que asumo como desarrollador freelance de Next.js: auditorías de caché, correcciones y migraciones a App Router.