tutoriales.com

¡Rendimiento Extremo! Cacheo de Datos Avanzado en Next.js con el App Router 🚀

Este tutorial te guiará a través de las técnicas de cacheo de datos más potentes en Next.js utilizando el App Router. Aprenderás a aprovechar el caché de peticiones, la revalidación on-demand y la configuración del caché para construir aplicaciones extremadamente rápidas y con una gran experiencia de usuario. Ideal para desarrolladores que buscan optimizar el rendimiento de sus proyectos Next.js.

Avanzado15 min de lectura6 views
Reportar error

La gestión eficiente del cacheo de datos es fundamental para construir aplicaciones web de alto rendimiento. Next.js, especialmente con la introducción del App Router, ha elevado el listón, ofreciendo herramientas robustas para controlar cómo y cuándo se guardan y revalidan los datos. Este tutorial explorará en profundidad estas capacidades, permitiéndote llevar el rendimiento de tu aplicación Next.js al siguiente nivel.

¿Por Qué el Cacheo es Crucial en Next.js? 🤔

El cacheo no es solo una buena práctica, es una necesidad en el desarrollo web moderno. Reduce la carga en tus servidores, disminuye la latencia y mejora significativamente la velocidad de carga para tus usuarios. En el contexto de Next.js, donde el fetching de datos puede ocurrir tanto en el servidor como en el cliente, entender y aplicar estrategias de caché es vital.

El App Router de Next.js introduce un modelo de programación más flexible que combina las ventajas del Server-Side Rendering (SSR), Static Site Generation (SSG) y Client-Side Rendering (CSR) de una manera unificada. Central a este modelo es un caché de datos unificado que opera tanto en el lado del servidor como en el lado del cliente, gestionando las peticiones fetch de forma inteligente.

💡 Consejo: Un buen cacheo puede hacer que una aplicación se sienta instantánea, incluso cuando la fuente de datos subyacente es lenta.

El Caché de Peticiones (fetch) en Next.js ⚡

Next.js extiende la API fetch nativa de JavaScript para proporcionar un mecanismo de caché potente y configurable. Cuando utilizas fetch dentro de un Server Component o en un route.ts (API Route), Next.js automáticamente cachea los resultados. Esto significa que si realizas la misma petición varias veces dentro de la misma solicitud o entre solicitudes, Next.js puede servir los datos desde el caché en lugar de ir a la fuente original.

Esta es la base de la reutilización de datos y es especialmente potente para:

  • Peticiones a la misma API dentro de diferentes Server Components.
  • Peticiones que se repiten en distintas páginas que comparten los mismos datos.

Controlando el Cacheo con fetch y Opciones 🛠️

La API fetch en Next.js permite un control granular sobre el comportamiento del caché mediante un segundo argumento de opciones. Aquí están las más importantes:

  • cache: 'force-cache' (predeterminado): Next.js intentará usar una versión cacheada de los datos si está disponible. Si no, los fetchea y los cachea.
  • cache: 'no-store' (o revalidate: 0): No cachea los datos y siempre los fetchea desde la fuente original.
  • revalidate: number: Cachea los datos durante number segundos. Después de ese tiempo, Next.js intentará refetchear los datos en segundo plano en la siguiente solicitud.

Veamos cómo aplicarlas.

// pages/productos/page.js (o un Server Component)
async function getProductos() {
  const res = await fetch('https://api.ejemplo.com/productos', {
    // Predeterminado, pero explícito para claridad
    cache: 'force-cache' 
  });
  if (!res.ok) {
    throw new Error('Fallo al obtener los productos');
  }
  return res.json();
}

export default async function ProductosPage() {
  const productos = await getProductos();
  // Renderiza tus productos
  return (
    <div>
      <h1>Nuestros Productos</h1>
      <ul>
        {productos.map(producto => (
          <li key={producto.id}>{producto.nombre}</li>
        ))}
      </ul>
    </div>
  );
}

El ejemplo anterior utilizará el caché por defecto. Si necesitas que los datos estén más frescos, puedes usar revalidate.

async function getArticulosRecientes() {
  const res = await fetch('https://api.ejemplo.com/articulos', {
    next: { revalidate: 60 } // Revalida cada 60 segundos
  });
  if (!res.ok) {
    throw new Error('Fallo al obtener los artículos');
  }
  return res.json();
}

export default async function ArticulosPage() {
  const articulos = await getArticulosRecientes();
  // ...
}

Aquí, los datos se cachearán por 60 segundos. Si un usuario visita la página después de que hayan pasado 60 segundos, Next.js mostrará los datos cucados (stale) y realizará una nueva petición en segundo plano para actualizar el caché para futuras solicitudes. Esto es conocido como Stale-While-Revalidate y es una técnica poderosa para ofrecer un rendimiento percibido excelente.

Para datos que nunca deben ser cacheados (por ejemplo, tokens de sesión o datos altamente dinámicos):

async function getDatosSesion() {
  const res = await fetch('https://api.ejemplo.com/sesion', {
    cache: 'no-store' // No cachea los datos
  });
  if (!res.ok) {
    throw new Error('Fallo al obtener datos de sesión');
  }
  return res.json();
}

export default async function SesionPage() {
  const datosSesion = await getDatosSesion();
  // ...
}

Revalidación On-Demand con revalidatePath y revalidateTag 🔄

El control del caché no se limita a la duración. Next.js te permite invalidar el caché de forma manual y bajo demanda, lo cual es increíblemente útil cuando los datos subyacentes cambian (por ejemplo, después de una actualización en un CMS o una base de datos).

Para ello, Next.js proporciona dos funciones clave:

  • revalidatePath(path: string): Revalida la ruta especificada. Todas las peticiones fetch realizadas dentro de esa ruta y que hayan sido cacheadas serán invalidadas y refetcheadas en la siguiente solicitud.
  • revalidateTag(tag: string): Revalida todas las peticiones fetch que fueron etiquetadas con la tag especificada. Esto es mucho más granular y potente.

Estas funciones deben ser utilizadas dentro de una API Route (un route.ts en el App Router) o en una Server Action, ya que necesitan ejecutar código en el servidor. No puedes llamarlas desde un Client Component.

🔥 Importante: La revalidación on-demand es la clave para mantener tus datos frescos sin sacrificar el rendimiento, especialmente en sitios con contenido dinámico que no cambia constantemente.

Ejemplo: Revalidación por Ruta

Imagina que tienes una página de blog (/blog) que muestra artículos. Cuando un nuevo artículo se publica o se actualiza, quieres que Next.js obtenga los datos más recientes en la siguiente visita.

Primero, tu fetch original en tu Server Component (o page.js):

// app/blog/page.js
async function getPosts() {
  const res = await fetch('https://api.ejemplo.com/posts', {
    next: { revalidate: 3600 } // Revalida cada hora
  });
  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();
  // ... render posts
}

Ahora, crea una API Route que pueda ser llamada, por ejemplo, por un webhook de tu CMS cuando un post se actualiza:

// app/api/revalidate-blog/route.js
import { revalidatePath } from 'next/cache';
import { NextResponse } from 'next/server';

export async function GET(request) {
  const secret = request.nextUrl.searchParams.get('secret');

  // Asegúrate de que el secreto coincida con tu variable de entorno
  if (secret !== process.env.MY_SECRET_TOKEN) {
    return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
  }

  try {
    revalidatePath('/blog');
    return NextResponse.json({ revalidated: true, now: Date.now() });
  } catch (err) {
    return NextResponse.json({ message: 'Error revalidating', error: err }, { status: 500 });
  }
}

Cuando se acceda a /api/revalidate-blog?secret=MY_SECRET_TOKEN, la caché de la ruta /blog será invalidada. La próxima vez que un usuario visite /blog, los datos se refetchearán.

Ejemplo: Revalidación por Etiqueta (Tag) 🏷️

La revalidación por etiqueta es más potente porque permite invalidar grupos de datos relacionados, independientemente de la ruta específica donde se muestren. Para usarla, necesitas asignar etiquetas a tus peticiones fetch.

// app/productos/[slug]/page.js
async function getProducto(slug) {
  const res = await fetch(`https://api.ejemplo.com/productos/${slug}`, {
    next: { tags: ['productos', `producto-${slug}`], revalidate: 3600 } 
  });
  if (!res.ok) {
    throw new Error('Failed to fetch product');
  }
  return res.json();
}

export default async function ProductPage({ params }) {
  const producto = await getProducto(params.slug);
  // ... render product details
}

Ahora, tu API Route para revalidar podría ser:

// app/api/revalidate-products/route.js
import { revalidateTag } from 'next/cache';
import { NextResponse } from 'next/server';

export async function GET(request) {
  const secret = request.nextUrl.searchParams.get('secret');
  const tag = request.nextUrl.searchParams.get('tag'); // e.g., 'productos' o 'producto-mi-slug'

  if (secret !== process.env.MY_SECRET_TOKEN) {
    return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
  }

  if (!tag) {
    return NextResponse.json({ message: 'Tag parameter is required' }, { status: 400 });
  }

  try {
    revalidateTag(tag); // Invalida todas las peticiones con esta etiqueta
    return NextResponse.json({ revalidated: true, now: Date.now(), tag });
  } catch (err) {
    return NextResponse.json({ message: 'Error revalidating', error: err }, { status: 500 });
  }
}

Con esto, puedes invalidar todos los productos (tag: 'productos') o un producto específico (tag: 'producto-mi-slug') cuando sea necesario. Esto es increíblemente flexible.


Configuración del Caché a Nivel de Ruta (Route Segment Config) ⚙️

Además del control granular con fetch, Next.js permite configurar el comportamiento de caché para segmentos de ruta completos. Esto se hace exportando una variable revalidate desde el archivo layout.js o page.js de un segmento.

// app/dashboard/layout.js o app/dashboard/page.js
export const revalidate = 60; // Revalida cada 60 segundos para este segmento y sus hijos

export default function DashboardLayout({ children }) {
  return (
    <div>
      <h2>Dashboard</h2>
      {children}
    </div>
  );
}

Este revalidate a nivel de segmento funciona de manera similar al revalidate en fetch, pero aplica a todas las peticiones de datos (incluyendo fetch, pero también otras fuentes si Next.js las gestionara) dentro de ese segmento.

También puedes establecer export const dynamic = 'force-dynamic' para forzar que una ruta o layout siempre se renderice dinámicamente en tiempo de solicitud, deshabilitando completamente el caché para ese segmento.

📌 Nota: Si tienes `revalidate` en `fetch` y `revalidate` a nivel de segmento, el `revalidate` en `fetch` tiene prioridad para esa petición específica. El `revalidate` a nivel de segmento actúa como un valor predeterminado para las peticiones que no especifican su propio `revalidate` o `cache` explícitamente.

Prioridad del Cacheo en Next.js App Router

Es importante entender cómo Next.js decide qué estrategia de caché aplicar. La prioridad, de mayor a menor, es generalmente la siguiente:

  1. cache: 'no-store' en fetch: Siempre refresca los datos.
  2. revalidate: 0 en fetch: Siempre refresca los datos.
  3. revalidate: number en fetch: Cachea por el tiempo especificado.
  4. export const revalidate = number a nivel de segmento: Cachea por el tiempo especificado (afecta a todas las fetch sin revalidate o cache explícito).
  5. cache: 'force-cache' en fetch (por defecto): Utiliza el caché si está disponible.
  6. export const dynamic = 'force-dynamic' a nivel de segmento: Fuerza el comportamiento dinámico (no cacheado) para el segmento.
⚠️ Advertencia: Una configuración incorrecta del caché puede llevar a datos desactualizados o a una sobrecarga del servidor. Planifica cuidadosamente tu estrategia de cacheo.

Patrones Avanzados y Casos de Uso ✨

Cacheo de Datos Compartidos Globalmente

Para datos que rara vez cambian y son necesarios en toda la aplicación (como la configuración del sitio, un menú de navegación estático), el cacheo estático (revalidate: false o simplemente el comportamiento predeterminado de force-cache en fetch) es ideal. Estos datos pueden ser obtenidos en un layout.js raíz y serán cacheados y compartidos de forma eficiente.

Integración con Server Actions para Mutaciones

Cuando utilizas Server Actions para mutar datos (por ejemplo, añadir un elemento a un carrito, actualizar un perfil), puedes y debes usar revalidatePath o revalidateTag dentro de la Server Action para asegurar que la UI se actualice con los datos más recientes.

// app/actions.js
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function addProductToCart(productId) {
  // Lógica para añadir producto al carrito en la DB
  await db.addToCart(productId);
  
  // Revalidar el carrito para que se muestre actualizado
  revalidatePath('/cart'); 
  revalidateTag('cart-items'); // Si tus peticiones de carrito tienen esta etiqueta
}

Diagrama de Flujo del Caché de Next.js fetch

Entender cómo fetch interactúa con el caché es clave.

Llamada a fetch() en Next.js Server Component / Route Handler ¿Cache configurado con revalidate > 0 o force-cache? No ¿Datos en caché y válidos? Servir desde Caché No ¿Stale-While-Revalidate permitido (revalidate > 0)? No Servir datos Stale desde Caché Segundo plano Petición a la API externa Almacenar / Actualizar Caché Servir datos Nuevos

Este diagrama muestra la lógica detrás de las decisiones de cacheo de Next.js, priorizando la entrega rápida de contenido mientras se busca la frescura cuando sea apropiado.


Buenas Prácticas y Consideraciones Avanzadas 💡

  • Granularidad de revalidateTag: Utiliza etiquetas específicas (e.g., producto-123) para invalidar solo lo que necesitas, y etiquetas más generales (e.g., productos) para invalidar colecciones. Esto minimiza el refetching innecesario.
  • Entorno de Desarrollo vs. Producción: El cacheo en desarrollo (con next dev) puede no comportarse exactamente como en producción (con next start). Siempre prueba tus estrategias de cacheo en un entorno de producción o staging.
  • HTTP Caching Headers: Next.js también respeta y emite HTTP caching headers (como Cache-Control) para recursos estáticos y rutas de API. Entender cómo interactúan con el caché de Next.js y los CDNs es crucial para una estrategia holística.
  • Monitoreo: Implementa monitoreo para ver la eficacia de tu caché. ¿Están tus cachés siendo golpeados? ¿Están los datos frescos? Herramientas como Vercel Analytics pueden proporcionar información valiosa.
  • Evita el Over-Caching: No caches datos que cambian constantemente o que son sensibles a las credenciales del usuario (sin una estrategia de no-store específica). Un mal cacheo puede exponer información o mostrar datos incorrectos.
  • Server Actions para Mutations: Siempre que modifiques datos, considera usar Server Actions combinadas con revalidatePath/revalidateTag para garantizar que la UI refleje el estado más reciente.
¿Qué pasa con `getServerSideProps` o `getStaticProps`? Con el App Router, `getServerSideProps` y `getStaticProps` de Pages Router son reemplazados por el modelo de *fetching* de datos de Server Components. Las configuraciones `revalidate` en `fetch` y a nivel de segmento cubren la mayoría de los casos de uso que antes requerían esas funciones, con mayor flexibilidad y una API unificada.
¡Caché de Next.js Dominado!

Dominar las estrategias de cacheo en Next.js con el App Router es una habilidad esencial para cualquier desarrollador que aspire a construir aplicaciones de alto rendimiento. Al comprender y aplicar fetch con sus opciones de caché, y utilizando revalidatePath y revalidateTag de manera inteligente, podrás ofrecer una experiencia de usuario excepcional, reducir la carga del servidor y optimizar tus costos de infraestructura.

Practica con estos conceptos en tus propios proyectos y observa cómo tus aplicaciones se vuelven más rápidas y eficientes.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!