Optimización - Cache Avanzado
Ahora que entiendes los fundamentos y la revalidación, veamos técnicas avanzadas para hacer tu aplicación extremadamente rápida.
Request Memoization (Deduplicación)
Request Memoization elimina automáticamente peticiones duplicadas dentro del mismo render del servidor.
El problema
// app/producto/[id]/page.tsx
export default async function ProductoPage({ params }) {
const producto = await fetch(`/api/productos/${params.id}`)
return (
<div>
<ProductoHeader productoId={params.id} />
<ProductoInfo productoId={params.id} />
<ProductosRelacionados productoId={params.id} />
</div>
)
}
async function ProductoHeader({ productoId }) {
// Necesita info del producto
const producto = await fetch(`/api/productos/${productoId}`)
return <h1>{producto.nombre}</h1>
}
async function ProductoInfo({ productoId }) {
// También necesita info del producto
const producto = await fetch(`/api/productos/${productoId}`)
return <p>{producto.descripcion}</p>
}
async function ProductosRelacionados({ productoId }) {
// También necesita info del producto
const producto = await fetch(`/api/productos/${productoId}`)
const relacionados = await fetch(`/api/productos/relacionados?id=${productoId}`)
return <div>{/* relacionados */}</div>
}
Sin memoization: 4 peticiones HTTP idénticas para el mismo producto.
Con memoization (automático): NextJS detecta que todas piden la misma URL y reutiliza la respuesta. Solo 1 petición HTTP real.
Cómo funciona
NextJS mantiene un mapa de peticiones durante el render:
Render empieza
↓
fetch('/api/productos/1') → Petición real → Guarda en mapa
↓
fetch('/api/productos/1') → Encuentra en mapa → Reutiliza
↓
fetch('/api/productos/1') → Encuentra en mapa → Reutiliza
↓
Render termina → Limpia el mapa
Solo funciona con fetch
// ✅ Se deduplica automáticamente
const data = await fetch('https://api.com/datos')
// ❌ NO se deduplica (consulta directa a DB)
const data = await db.producto.findUnique({ where: { id } })
Para consultas de base de datos, usa unstable_cache
:
import { unstable_cache } from 'next/cache'
const obtenerProducto = unstable_cache(
async (id: string) => {
return await db.producto.findUnique({ where: { id } })
},
['producto'],
{ revalidate: 300 }
)
// Ahora estas llamadas se deduplicarán
const producto1 = await obtenerProducto('1')
const producto2 = await obtenerProducto('1')
const producto3 = await obtenerProducto('1')
// Solo 1 consulta real a la DB
Pasar datos vs. volver a obtener
// Opción 1: Pasar datos como props (tradicional)
export default async function ProductoPage({ params }) {
const producto = await fetch(`/api/productos/${params.id}`).then(r => r.json())
return (
<div>
<ProductoHeader producto={producto} />
<ProductoInfo producto={producto} />
<ProductosRelacionados producto={producto} />
</div>
)
}
function ProductoHeader({ producto }) {
return <h1>{producto.nombre}</h1>
}
// Opción 2: Cada componente obtiene sus datos (con memoization)
export default async function ProductoPage({ params }) {
return (
<div>
<ProductoHeader productoId={params.id} />
<ProductoInfo productoId={params.id} />
<ProductosRelacionados productoId={params.id} />
</div>
)
}
async function ProductoHeader({ productoId }) {
const producto = await fetch(`/api/productos/${productoId}`).then(r => r.json())
return <h1>{producto.nombre}</h1>
}
async function ProductoInfo({ productoId }) {
const producto = await fetch(`/api/productos/${productoId}`).then(r => r.json())
return <p>{producto.descripcion}</p>
}
Ambas opciones tienen la misma performance gracias a memoization. La opción 2 tiene ventajas:
- Cada componente es independiente
- Más fácil mover componentes
- Cada componente pide solo lo que necesita
Patrón recomendado
En Server Components, no te preocupes por "prop drilling" (pasar props por muchos niveles). Deja que cada componente obtenga sus propios datos. NextJS se encarga de la deduplicación.
Full Route Cache
El Full Route Cache guarda el HTML renderizado de tus páginas.
Páginas estáticas
// app/sobre-nosotros/page.tsx
export default function SobreNosotros() {
return (
<div>
<h1>Sobre Nosotros</h1>
<p>Fundada en 2020, somos una tienda de ropa...</p>
</div>
)
}
En build time:
npm run build
Route (app) Size First Load JS
┌ ○ /sobre-nosotros 385 B 82.3 kB
○ (Static) Renderizada como HTML estático
NextJS genera el HTML una vez y lo sirve a todos. Extremadamente rápido.
Páginas con datos estáticos
// app/productos/page.tsx
export default async function ProductosPage() {
const productos = await fetch('https://api.mitienda.com/productos', {
cache: 'force-cache' // Cachear para siempre
}).then(r => r.json())
return (
<div>
<h1>Productos</h1>
{productos.map(p => (
<div key={p.id}>{p.nombre}</div>
))}
</div>
)
}
Si no hay nada dinámico (cookies, headers, searchParams), NextJS genera HTML estático en build.
Páginas dinámicas
// app/carrito/page.tsx
import { cookies } from 'next/headers'
export default async function CarritoPage() {
const cookieStore = await cookies() // Usa cookies
const userId = cookieStore.get('userId')
// Esta página NO puede ser estática
const carrito = await fetch(`/api/carrito/${userId}`).then(r => r.json())
return <div>{/* carrito */}</div>
}
npm run build
Route (app) Size First Load JS
└ ƒ /carrito 891 B 86.7 kB
ƒ (Dynamic) Renderizada bajo demanda por cada request
NextJS detecta que usa cookies()
y la marca como dinámica.
Forzar comportamiento
// Forzar página dinámica (no cachear)
export const dynamic = 'force-dynamic'
// Forzar página estática (cachear)
export const dynamic = 'force-static'
// Intentar estática, pero permitir fallback a dinámica
export const dynamic = 'auto' // Default
// Error si intenta ser dinámica
export const dynamic = 'error'
Qué hace una página dinámica
Cosas que impiden que una página sea estática:
import { cookies, headers } from 'next/headers'
import { searchParams } from 'next/navigation'
// ❌ Estas funciones hacen la página dinámica:
const cookieStore = await cookies()
const headersList = await headers()
const params = useSearchParams() // En Client Components
// ❌ Fetch sin cache
fetch('url', { cache: 'no-store' })
// ❌ revalidate: 0
fetch('url', { next: { revalidate: 0 } })
// ❌ Math.random(), Date.now() en Server Components
const timestamp = Date.now()
Para mantener páginas estáticas, evita estas funciones.
Router Cache (Navegación del cliente)
El Router Cache guarda páginas visitadas en el navegador durante la sesión.
Cómo funciona
// Usuario navega por tu sitio
/productos → Carga del servidor, guarda en cache
/producto/1 → Carga del servidor, guarda en cache
/productos → ⚡ Instantáneo (cache del browser)
/producto/2 → Carga del servidor, guarda en cache
/producto/1 → ⚡ Instantáneo (cache del browser)
Duración del cache
Tipo de página | Duración en cache |
---|---|
Estática (sin datos) | 5 minutos |
Estática (con datos) | 5 minutos |
Dinámica | 30 segundos |
Forzar revalidación
'use client'
import { useRouter } from 'next/navigation'
export default function MiComponente() {
const router = useRouter()
function handleClick() {
// Limpia el cache del router y recarga la página actual
router.refresh()
}
return <button onClick={handleClick}>Recargar datos</button>
}
Prefetching
NextJS precarga páginas automáticamente cuando el link es visible:
import Link from 'next/link'
export default function ProductosPage({ productos }) {
return (
<div>
{productos.map(p => (
// NextJS precarga esta página cuando el link es visible
<Link href={`/productos/${p.id}`} key={p.id}>
{p.nombre}
</Link>
))}
</div>
)
}
Controlar prefetching:
// Prefetch activado (default)
<Link href="/productos/1" prefetch={true}>
Ver producto
</Link>
// Prefetch desactivado
<Link href="/productos/1" prefetch={false}>
Ver producto
</Link>
// Prefetch solo cuando pasa el mouse
<Link href="/productos/1" prefetch={null}>
Ver producto
</Link>
Para listas grandes, desactiva prefetch:
export default function ProductosPage({ productos }) {
return (
<div>
{productos.map(p => (
// 1000 productos = no precargar todos
<Link
href={`/productos/${p.id}`}
key={p.id}
prefetch={false}
>
{p.nombre}
</Link>
))}
</div>
)
}
generateStaticParams - Páginas dinámicas estáticas
Para rutas dinámicas que quieres pre-renderizar en build time.
Sin generateStaticParams
// app/productos/[id]/page.tsx
export default async function ProductoPage({ params }) {
const producto = await fetch(`/api/productos/${params.id}`).then(r => r.json())
return <div>{producto.nombre}</div>
}
npm run build
Route (app) Size First Load JS
└ ƒ /productos/[id] 1.2 kB 84.1 kB
ƒ (Dynamic) Se genera cuando alguien la visita
Todas las páginas de productos se generan on-demand.
Con generateStaticParams
// app/productos/[id]/page.tsx
// Genera páginas estáticas para estos productos
export async function generateStaticParams() {
const productos = await fetch('https://api.mitienda.com/productos')
.then(r => r.json())
return productos.map(producto => ({
id: producto.id.toString()
}))
}
export default async function ProductoPage({ params }) {
const producto = await fetch(`/api/productos/${params.id}`, {
cache: 'force-cache'
}).then(r => r.json())
return <div>{producto.nombre}</div>
}
npm run build
Route (app) Size First Load JS
└ ● /productos/[id] (100) 1.2 kB 84.1 kB
● (SSG) Generadas 100 páginas en build
NextJS genera 100 páginas HTML en build. Súper rápidas.
Estrategia: Solo productos populares
export async function generateStaticParams() {
// Solo genera páginas para los 50 productos más vendidos
const populares = await fetch('https://api.mitienda.com/productos/populares')
.then(r => r.json())
return populares.slice(0, 50).map(p => ({
id: p.id.toString()
}))
}
// Permitir generar páginas on-demand para otros productos
export const dynamicParams = true // Default en v15
Resultado:
- 50 productos populares: HTML pre-generado (instantáneo)
- Otros productos: Generados on-demand la primera vez, luego cacheados
Rutas anidadas
// app/categorias/[categoriaId]/productos/[productoId]/page.tsx
export async function generateStaticParams() {
const categorias = await fetch('https://api.mitienda.com/categorias')
.then(r => r.json())
const paths = []
for (const cat of categorias) {
const productos = await fetch(`https://api.mitienda.com/productos?cat=${cat.id}`)
.then(r => r.json())
productos.forEach(p => {
paths.push({
categoriaId: cat.id.toString(),
productoId: p.id.toString()
})
})
}
return paths
}
export default async function ProductoPage({ params }) {
const { categoriaId, productoId } = params
// ...
}
Limitar generación
export async function generateStaticParams() {
const productos = await fetch('https://api.mitienda.com/productos')
.then(r => r.json())
// Solo genera las primeras 100
return productos.slice(0, 100).map(p => ({
id: p.id.toString()
}))
}
// Comportamiento para IDs no generados
export const dynamicParams = true // Genera on-demand (default)
// export const dynamicParams = false // 404 error
Estrategia óptima
Usa generateStaticParams
para:
- Páginas más visitadas (top 100 productos)
- Páginas más importantes (categorías principales)
- Páginas con contenido que raramente cambia
Combina con revalidate
para actualizaciones automáticas y obtienes lo mejor de ambos mundos.
Estrategias de rendering
Static Rendering (default)
Genera HTML en build time. Más rápido posible.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.miblog.com/posts').then(r => r.json())
return posts.map(p => ({ slug: p.slug }))
}
export const revalidate = 3600 // ISR: actualiza cada hora
export default async function BlogPost({ params }) {
const post = await fetch(`https://api.miblog.com/posts/${params.slug}`, {
next: { tags: [`post-${params.slug}`] }
}).then(r => r.json())
return (
<article>
<h1>{post.titulo}</h1>
<div dangerouslySetInnerHTML={{ __html: post.contenido }} />
</article>
)
}
Ventajas:
- Extremadamente rápido (HTML pre-generado)
- Barato (no ejecuta código en cada request)
- Excelente para SEO
Desventajas:
- Datos pueden quedar desactualizados
- Build puede ser lento con muchas páginas
Dynamic Rendering
Genera HTML en cada request. Siempre actualizado.
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'
export default async function Dashboard() {
const usuario = await obtenerUsuarioActual()
const estadisticas = await fetch(`/api/estadisticas/${usuario.id}`, {
cache: 'no-store'
}).then(r => r.json())
return (
<div>
<h1>Dashboard de {usuario.nombre}</h1>
<div>Ventas hoy: ${estadisticas.ventasHoy}</div>
</div>
)
}
Ventajas:
- Siempre actualizado
- Puede personalizar por usuario
- Acceso a cookies, headers, etc
Desventajas:
- Más lento (ejecuta código en cada request)
- Más costoso en servidor
Streaming con Suspense
Muestra partes de la página progresivamente:
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Shell de la página se muestra inmediatamente */}
<div className="grid grid-cols-2 gap-4">
{/* Cada tarjeta carga independientemente */}
<Suspense fallback={<CardSkeleton />}>
<VentasCard />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<ProductosCard />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<UsuariosCard />
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<PedidosCard />
</Suspense>
</div>
</div>
)
}
// Cada componente async carga sus propios datos
async function VentasCard() {
const ventas = await fetch('https://api.mitienda.com/ventas').then(r => r.json())
return (
<div className="border rounded p-4">
<h2>Ventas</h2>
<p className="text-3xl">${ventas.total}</p>
</div>
)
}
async function ProductosCard() {
await new Promise(resolve => setTimeout(resolve, 2000)) // Simula lentitud
const productos = await fetch('https://api.mitienda.com/productos').then(r => r.json())
return (
<div className="border rounded p-4">
<h2>Productos</h2>
<p className="text-3xl">{productos.length}</p>
</div>
)
}
function CardSkeleton() {
return (
<div className="border rounded p-4 animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/2 mb-4"></div>
<div className="h-8 bg-gray-200 rounded"></div>
</div>
)
}
Qué sucede:
- Usuario carga la página
- Estructura básica aparece inmediatamente
- Tarjetas aparecen progresivamente cuando terminan de cargar
- Usuario no ve pantalla en blanco
Ventajas:
- Mejor UX (contenido progresivo)
- Time to First Byte (TTFB) más rápido
- Usuario ve algo inmediatamente
Partial Prerendering (PPR) - Experimental
Combina estático + dinámico en la misma página:
// next.config.js
module.exports = {
experimental: {
ppr: true
}
}
// app/producto/[id]/page.tsx
import { Suspense } from 'react'
export default async function ProductoPage({ params }) {
// Parte estática (pre-renderizada)
const producto = await fetch(`/api/productos/${params.id}`, {
cache: 'force-cache'
}).then(r => r.json())
return (
<div>
{/* Estático */}
<h1>{producto.nombre}</h1>
<p>{producto.descripcion}</p>
<p>${producto.precio}</p>
{/* Dinámico (renderizado en request) */}
<Suspense fallback={<div>Cargando stock...</div>}>
<StockDisponible productoId={params.id} />
</Suspense>
{/* Dinámico (personalizado por usuario) */}
<Suspense fallback={<div>Cargando recomendaciones...</div>}>
<ProductosRecomendados />
</Suspense>
</div>
)
}
// Componente dinámico
async function StockDisponible({ productoId }) {
const stock = await fetch(`/api/stock/${productoId}`, {
cache: 'no-store'
}).then(r => r.json())
return <p>Stock disponible: {stock.cantidad}</p>
}
NextJS pre-renderiza la parte estática y deja "huecos" para las partes dinámicas.
PPR es experimental
Partial Prerendering está en fase experimental. La API puede cambiar. Úsalo solo si te gusta vivir al límite.
Debugging de cache
Ver qué está cacheado
// app/productos/page.tsx
export default async function ProductosPage() {
// Timestamp para ver cuándo se generó
const generadoEn = new Date().toISOString()
const productos = await fetch('https://api.mitienda.com/productos')
.then(r => r.json())
return (
<div>
<p className="text-xs text-gray-500">
Página generada: {generadoEn}
</p>
<h1>Productos</h1>
{/* ... */}
</div>
)
}
Recarga varias veces. Si el timestamp no cambia, está cacheado.
Headers de respuesta
Abre DevTools → Network → Selecciona la página:
X-NextJS-Cache: HIT // Vino del cache
X-NextJS-Cache: MISS // No estaba en cache
X-NextJS-Cache: STALE // Cache viejo, regenerando
Cache-Control: s-maxage=3600, stale-while-revalidate=86400
Cache en build
npm run build
Route (app) Size First Load JS
┌ ○ / 385 B 82.3 kB
├ ○ /productos 2.1 kB 84.4 kB
├ ● /productos/[id] (50 entries) 1.2 kB 83.5 kB
├ ƒ /carrito 891 B 83.2 kB
└ ƒ /dashboard 1.5 kB 83.8 kB
Legend:
○ (Static) Pre-renderizada como HTML estático
● (SSG) Pre-renderizada como HTML (usa generateStaticParams)
ƒ (Dynamic) Renderizada por cada request
Force refresh en desarrollo
# Limpia cache de Next.js
rm -rf .next
# Reinstala dependencias y limpia cache
rm -rf .next node_modules
npm install
Desactivar cache en desarrollo
// next.config.js
module.exports = {
// Útil para debugging
experimental: {
isrMemoryCacheSize: 0 // Desactiva cache de ISR
}
}
Patrones avanzados de optimización
Patrón 1: Contenido above the fold estático
// app/landing/page.tsx
export default async function LandingPage() {
// Hero es estático (rápido)
return (
<>
<HeroSection />
<FeaturesSection />
{/* Testimonios dinámicos (carga después) */}
<Suspense fallback={<TestimoniosSkeleton />}>
<TestimoniosSection />
</Suspense>
{/* Estadísticas dinámicas */}
<Suspense fallback={<EstadisticasSkeleton />}>
<EstadisticasSection />
</Suspense>
</>
)
}
// Estático - pre-renderizado
function HeroSection() {
return (
<section>
<h1>Bienvenido a nuestra tienda</h1>
<p>Los mejores productos al mejor precio</p>
</section>
)
}
// Dinámico - datos actualizados
async function TestimoniosSection() {
const testimonios = await fetch('https://api.mitienda.com/testimonios/recientes', {
next: { revalidate: 60 }
}).then(r => r.json())
return (
<section>
{testimonios.map(t => (
<div key={t.id}>{t.texto}</div>
))}
</section>
)
}
Patrón 2: Cache por nivel de usuario
// lib/cache.ts
import { unstable_cache } from 'next/cache'
export function cacheParaTipoUsuario<T>(
fn: (userId: string) => Promise<T>,
cacheKey: string,
tipoUsuario: 'admin' | 'cliente'
) {
// Admins ven datos más frescos
const revalidate = tipoUsuario === 'admin' ? 60 : 300
return unstable_cache(fn, [cacheKey], { revalidate })
}
// Uso
const obtenerDashboard = cacheParaTipoUsuario(
async (userId) => {
return await db.dashboard.findUnique({ where: { userId } })
},
'dashboard',
usuario.tipo
)
Patrón 3: Preload de datos
// app/productos/[id]/page.tsx
// Preload (inicio de carga)
export async function generateMetadata({ params }) {
const producto = await fetch(`/api/productos/${params.id}`).then(r => r.json())
return {
title: producto.nombre,
description: producto.descripcion
}
}
// El fetch ya está en cache gracias a memoization
export default async function ProductoPage({ params }) {
const producto = await fetch(`/api/productos/${params.id}`).then(r => r.json())
return <div>{producto.nombre}</div>
}
NextJS empieza a cargar datos en generateMetadata
, y los reutiliza en el componente.
Patrón 4: Parallel data fetching
// ❌ Secuencial (lento)
async function DashboardPage() {
const ventas = await fetch('/api/ventas').then(r => r.json()) // 500ms
const usuarios = await fetch('/api/usuarios').then(r => r.json()) // 500ms
const productos = await fetch('/api/productos').then(r => r.json()) // 500ms
// Total: 1500ms
return <div>{/* ... */}</div>
}
// ✅ Paralelo (rápido)
async function DashboardPage() {
const [ventas, usuarios, productos] = await Promise.all([
fetch('/api/ventas').then(r => r.json()), // Todos en paralelo
fetch('/api/usuarios').then(r => r.json()),
fetch('/api/productos').then(r => r.json())
])
// Total: 500ms (el más lento de los 3)
return <div>{/* ... */}</div>
}
Patrón 5: Waterfall prevention con Suspense
// ❌ Waterfall: Cada nivel espera al anterior
async function ProductoPage({ params }) {
const producto = await fetch(`/api/productos/${params.id}`)
return (
<div>
<ProductoInfo producto={producto} />
<Suspense fallback={<div>Cargando reseñas...</div>}>
<Reseñas productoId={params.id} />
</Suspense>
</div>
)
}
async function Reseñas({ productoId }) {
// Espera a que producto termine
const reseñas = await fetch(`/api/reseñas/${productoId}`)
return <div>{/* reseñas */}</div>
}
// ✅ Sin waterfall: Todo en paralelo
async function ProductoPage({ params }) {
return (
<div>
<Suspense fallback={<div>Cargando producto...</div>}>
<ProductoInfo productoId={params.id} />
</Suspense>
<Suspense fallback={<div>Cargando reseñas...</div>}>
<Reseñas productoId={params.id} />
</Suspense>
</div>
)
}
// Ahora ambos fetch empiezan simultáneamente
async function ProductoInfo({ productoId }) {
const producto = await fetch(`/api/productos/${productoId}`)
return <div>{/* producto */}</div>
}
async function Reseñas({ productoId }) {
const reseñas = await fetch(`/api/reseñas/${productoId}`)
return <div>{/* reseñas */}</div>
}
Mejores prácticas
1. Mide antes de optimizar
// Agrega timestamps para medir
console.time('fetch-productos')
const productos = await fetch('/api/productos')
console.timeEnd('fetch-productos')
No optimices sin datos. Puede que no sea el cuello de botella.
2. Cache agresivamente, revalida inteligentemente
// ✅ Cache largo + revalidación manual
export const revalidate = 3600
async function agregarProducto() {
await db.producto.create(/* ... */)
revalidateTag('productos') // Actualiza inmediatamente
}
Mejor que cache corto que se regenera constantemente.
3. Separa estático de dinámico
// ✅ Layout estático, página dinámica
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div>
{/* Navegación estática */}
<nav>
<Link href="/dashboard">Inicio</Link>
<Link href="/dashboard/productos">Productos</Link>
</nav>
{/* Contenido dinámico */}
{children}
</div>
)
}
4. Usa loading.tsx para UX
// app/productos/loading.tsx
export default function Loading() {
return (
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 9 }).map((_, i) => (
<div key={i} className="animate-pulse">
<div className="h-48 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded"></div>
</div>
))}
</div>
)
}
Usuarios prefieren ver loading que pantalla en blanco.
5. Monitorea el tamaño de build
npm run build
# Busca páginas grandes
Route (app) Size First Load JS
├ ○ /productos 25 kB ⚠️ 108 kB # DEMASIADO GRANDE
Si una página estática es >100 kB, considera dividirla.
6. Usa generateStaticParams con límite
// ❌ 10,000 productos = build de 30 minutos
export async function generateStaticParams() {
const productos = await fetch('/api/productos').then(r => r.json())
return productos.map(p => ({ id: p.id }))
}
// ✅ Solo 100 productos = build de 30 segundos
export async function generateStaticParams() {
const productos = await fetch('/api/productos/populares').then(r => r.json())
return productos.slice(0, 100).map(p => ({ id: p.id }))
}
export const dynamicParams = true // Otros on-demand
7. Cache de queries complejas
import { unstable_cache } from 'next/cache'
const obtenerEstadisticas = unstable_cache(
async () => {
// Query pesada con múltiples joins
return await db.$queryRaw`
SELECT
COUNT(*) as total_ventas,
SUM(total) as ingresos,
AVG(total) as ticket_promedio
FROM ventas
WHERE fecha > NOW() - INTERVAL '30 days'
`
},
['estadisticas-mensuales'],
{ revalidate: 3600 }
)
Checklist de optimización
Antes de desplegar a producción:
Build time:
- Ejecuta
npm run build
y revisa el output - Verifica que páginas importantes son estáticas (○ o ●)
- Confirma que páginas dinámicas realmente necesitan serlo (ƒ)
- First Load JS < 100 kB para páginas principales
Runtime:
- Configura
revalidate
apropiado para cada página - Implementa revalidación on-demand para mutaciones
- Usa tags para invalidación granular
- Agrega
loading.tsx
a rutas lentas
Queries:
- Usa
unstable_cache
para queries de DB complejas - Implementa
generateStaticParams
para rutas dinámicas populares - Configura
cache: 'force-cache'
en fetch cuando apropiado - Parallel fetching con
Promise.all()
cuando sea posible
UX:
- Implementa Suspense para contenido above-the-fold rápido
- Skeletons/placeholders para estados de carga
- Prefetch desactivado en listas largas
-
router.refresh()
donde usuarios esperan datos frescos
Resumen
Request Memoization:
- Automático, elimina duplicados en el mismo render
- Solo funciona con
fetch()
- Para DB usa
unstable_cache
Full Route Cache:
- HTML pre-generado en build
- Usa
generateStaticParams
para rutas dinámicas - Combina con ISR para actualizaciones automáticas
Router Cache:
- Navegación instantánea en el cliente
- Controla con
prefetch
en Link - Limpia con
router.refresh()
Estrategias:
- Static cuando puedas
- Dynamic cuando debas
- Streaming para mejor UX
- PPR para lo mejor de ambos mundos (experimental)
Performance óptima:
- Mide primero
- Cache agresivamente
- Revalida inteligentemente
- Separa estático de dinámico
- Usa Suspense para contenido progresivo
Con estas técnicas, tu aplicación NextJS será extremadamente rápida. 🚀