ISR + generateStaticParams + invalidación on-demand, juntos
Por separado son 3 conceptos simples. Combinados correctamente, dan páginas de producto casi instantáneas que igual reflejan cambios de stock al momento.
Cada uno de estos tres conceptos, por separado, es relativamente simple de entender. La parte interesante —y donde la mayoría de tutoriales se quedan cortos— es verlos trabajar juntos en un caso real: una página de producto que carga casi instantánea (porque está pre-renderizada), pero que igual refleja un cambio de stock sin esperar ningún tiempo de cache.
Paso 1: generateStaticParams decide qué páginas existen de antemano
// src/app/producto/[slug]/page.tsx
export async function generateStaticParams() {
const products = await getProducts()
return products.map((product) => ({ slug: product.slug }))
}
Esta función corre en build time (y también cuando Next.js necesita regenerar el listado de rutas). Le dice al framework exactamente qué valores de [slug] existen, para poder pre-renderizar una página HTML completa por cada uno — en vez de esperar a que un usuario la pida para recién generarla.
Paso 2: cada página individual usa cache con tags
export const getProductBySlug = cache(
unstable_cache(
async (slug: string) => {
return prisma.product.findUnique({ where: { slug } })
},
['product-by-slug'],
{ tags: ['products'], revalidate: 3600 }
)
)
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const product = await getProductBySlug(slug)
// ...
}
Cada página de producto individual queda respaldada por el Data Cache (unstable_cache) y por el Full Route Cache (heredado del hecho de ser una ruta pre-renderizada). Con revalidate: 3600, el HTML de cada producto se considera válido hasta por una hora — tiempo suficiente para servir instantáneo a miles de visitas sin volver a tocar la base de datos.
Paso 3: el evento que rompe la espera de una hora
Hasta aquí, tienes páginas rápidas que se actualizan solas cada hora. Pero un cliente que acaba de comprar el último producto en stock no debería tener que esperar hasta una hora para que la página deje de mostrar “disponible”:
'use server'
import { updateTag } from 'next/cache'
export async function submitCheckout(/* ...datos del pedido... */) {
await prisma.$transaction(async (tx) => {
// crea la orden, descuenta stock
})
updateTag('products') // invalida TODO lo etiquetado 'products', ya
}
Con esto, la próxima visita a ese producto específico — sea 3 segundos después o 3 horas después de la compra — dispara una regeneración real, en vez de servir el HTML “congelado” con el stock viejo.
El punto clave: generateStaticParams no vuelve a correr por cada cambio de stock
Un malentendido común: pensar que cada vez que llamas updateTag, Next.js vuelve a ejecutar generateStaticParams para todo el sitio. No es así — generateStaticParams decide qué rutas existen, no su contenido. El stock de un producto existente cambiando no crea ni elimina ninguna ruta; solo invalida el contenido cacheado de una ruta que ya existía. generateStaticParams solo vuelve a ejecutarse en un nuevo build, o cuando aparece/desaparece un producto (un slug nuevo o eliminado).
Qué pasa con un producto que se agrega después del build
Si agregas un producto nuevo a la base de datos después de que el sitio ya está desplegado, ese slug no estaba en la lista de generateStaticParams del último build. Por defecto, Next.js maneja esto de forma elegante: la primera visita a esa ruta nueva se renderiza on-demand (como si fuera dinámica esa primera vez), y a partir de ahí queda cacheada igual que el resto — sin que hayas necesitado un redeploy completo del sitio para que el producto nuevo esté disponible.
Combinando los tres: la línea de tiempo completa
- Build time:
generateStaticParamsgenera el HTML de cada producto existente. - Primera hora: cualquier visita sirve ese HTML pre-generado, instantáneo, sin tocar la base de datos.
- Alguien compra: la Server Action del checkout llama
updateTag('products'), invalidando el Data Cache y el Full Route Cache de todos los productos y el listado, de un solo golpe. - Siguiente visita a ese producto: Next.js detecta que el cache está inválido, ejecuta la query real, regenera el HTML, y lo vuelve a cachear para las próximas visitas.
- Producto nuevo agregado: la primera visita a su
slugse renderiza on-demand, sin esperar un redeploy.
Todo esto sin que el usuario final perciba ninguna diferencia de velocidad entre “producto que llevaba una hora sin cambios” y “producto que acaba de venderse” — ambos casos sirven contenido rápido, solo que uno viene de cache y el otro se regeneró segundos antes.
Cuándo este patrón no es el adecuado
Si tu catálogo cambia constantemente (por ejemplo, precios que fluctúan cada minuto como en un sistema de trading), el costo de mantener este esquema de invalidación por tags puede no valer la pena frente a simplemente usar SSR puro para esas rutas específicas. ISR con invalidación on-demand brilla quando los cambios son eventos discretos y relativamente poco frecuentes (una compra, una edición de admin) sobre un catálogo que, la mayoría del tiempo, es estable.
Conclusión
generateStaticParams, ISR y la invalidación on-demand no son tres features independientes que casualmente coexisten — son tres piezas de un mismo diseño: pre-renderiza todo lo que puedas de antemano, sirve eso rápido por defecto, e invalida quirúrgicamente solo cuando un evento de negocio real lo justifica. Ese es el balance entre performance y frescura que la mayoría de e-commerce necesitan.
¿Tu catálogo de productos podría beneficiarse de esta estrategia de cache? Agenda una asesoría técnica gratuita de 15 minutos y lo evaluamos juntos.
¿Tienes un proyecto en mente?
Convierte tu idea en un producto real
Desarrollo web, aplicaciones a medida y consultoría tecnológica para empresas y startups. Cuéntame tu proyecto y te respondo en menos de 24 horas.