# Ivan Bongiovanni - Full Stack Engineer & Next.js Expert (/es)
{/* Live Activity Widgets */}
Trayectoria [#trayectoria]
Propuesta de Valor [#propuesta-de-valor]
Tech Stack [#tech-stack]
En resumen [#en-resumen]
# Index (/es/blog)
Últimos artículos [#últimos-artículos]
Demo de Cacheo Granular: Next.js 16 [#demo-de-cacheo-granular-nextjs-16]
Tutorial práctico: Cómo implementar `use cache` y `cacheTag` para mezclar contenido estático y dinámico en la misma vista.
**Temas:** Next.js 16 · PPR · Granular Caching
***
Arquitectura Escalable en Next.js 15: La Regla del Scope [#arquitectura-escalable-en-nextjs-15-la-regla-del-scope]
Descubre cómo organizar tu proyecto para que escale sin dolor. Deuda técnica cero y onboarding instantáneo.
**Temas:** Arquitectura · Clean Code · Scalability
***
Guía de Renderizado en Next.js: SSR, SSG, ISR y PPR [#guía-de-renderizado-en-nextjs-ssr-ssg-isr-y-ppr]
No adivines qué renderizado usar. Una guía técnica para elegir entre Server-Side, Static, Incremental o Partial Prerendering.
**Temas:** Performance · Core Web Vitals · Rendering Patterns
***
Sobre este blog [#sobre-este-blog]
Escribo sobre temas que me apasionan:
* **Arquitectura de aplicaciones** - Cómo estructurar proyectos escalables
* **Performance web** - Optimización y mejores prácticas
* **Next.js y React** - Nuevas features y patrones
* **TypeScript** - Tipado estricto y buenas prácticas
* **IA aplicada** - Integración de LLMs en productos
¿Tenés algún tema que te gustaría que cubra? [Contactame](/).
# Guía de Renderizado en Next.js: SSR, SSG, ISR y PPR (/es/blog/nextjs-renders)
La elección de la estrategia de renderizado no es solo una decisión técnica; define la **Experiencia de Usuario (UX)**, el **SEO** y los **costos de infraestructura** de tu aplicación.
Esta guía técnica desglosa cuándo y por qué utilizar cada modelo en el ecosistema actual de Next.js.
***
SSR (Server-Side Rendering) [#ssr-server-side-rendering]
*Dynamic Rendering at Request Time*
El HTML se genera en el servidor **en cada petición**.
**Ideal para:**
* **Datos en tiempo real**: Dashboards financieros, feeds de redes sociales.
* **Personalización crítica**: Páginas que dependen de cookies o headers del request.
* **SEO dinámico**: Contenido que cambia cada segundo y debe ser indexado.
**Trade-offs Técnicos:**
* ⚠️ **Mayor TTFB (Time to First Byte)**: El usuario espera a que el servidor procese la página.
* 💸 **Costo Computacional**: Requiere ejecución de servidor (Serverless/Node) por cada visita.
***
SSG (Static Site Generation) [#ssg-static-site-generation]
*Build Once, Cache Forever*
El HTML se genera durante el **build time** y se distribuye globalmente vía CDN.
**Ideal para:**
* **Marketing Pages**: Landings, "About Us", Contacto.
* **Documentación**: Contenido que no cambia frecuentemente.
* **Blogs**: Artículos históricos.
**Ventajas:**
* 🚀 **Performance Extrema**: El TTFB es insignificante (servido desde el Edge).
* ✅ **Estabilidad**: Si la API de datos falla, la página sigue funcionando (es un archivo estático).
***
ISR (Incremental Static Regeneration) [#isr-incremental-static-regeneration]
*Static Content with Dynamic Updates*
Combina la velocidad de SSG con la frescura de SSR. Permite regenerar páginas estáticas en el fondo tras un intervalo de tiempo (`revalidate`).
**Ideal para:**
* **E-commerce**: Fichas de producto (precios/stock pueden tener un lag de minutos).
* **Sitios de Noticias**: Contenido que puede tolerar un retraso de 60 segundos.
**Mecanismo:**
1. Usuario A visita la página → Recibe versión v1 (rápida desde caché).
2. Next.js detecta que el caché expiró → Inicia regeneración en background.
3. Usuario B visita la página → Recibe v2 actualizada.
***
PPR (Partial Prerendering) [#ppr-partial-prerendering]
*The Future of Hybrid Rendering*
La "bala de plata" de Next.js. Permite que una misma ruta tenga un **shell estático** (SSG) de carga instantánea, mientras partes dinámicas (como un carrito de compras) se cargan vía **Streaming**.
**Ideal para:**
* **Aplicaciones Modernas**: Donde el layout es estático pero el contenido es dinámico.
* **Optimización de Core Web Vitals**: Mejora drástica de LCP (Largest Contentful Paint) y reducce el CLS.
**Beneficio:** Elimina el "waterfall" de datos. El usuario ve la estructura inmediatamente mientras los datos llegan.
***
Matriz de Decisión [#matriz-de-decisión]
| Estrategia | Escenario Típico | Performance (LCP) | Costo Server |
| :--------- | :----------------- | :------------------ | :------------ |
| **SSG** | Blog, Landing | ⭐⭐⭐⭐⭐ (Instantáneo) | 📉 Bajo |
| **ISR** | Catálogo Productos | ⭐⭐⭐⭐⭐ (Cache Hit) | 📉 Bajo-Medio |
| **SSR** | Admin Panel, Feed | ⭐⭐ (Depende de DB) | 📈 Alto |
| **PPR** | App Compleja | ⭐⭐⭐⭐ (Hybrid) | ⚖️ Balanceado |
***
Conclusión [#conclusión]
No existe un "mejor renderizado" universal.
* Si puedes hacerlo estático (**SSG**), hazlo estático.
* Si necesitas frescura pero puedes tolerar latencia, usa **SSR**.
* Si quieres lo mejor de ambos mundos para apps complejas, apuesta por **PPR**.
Como ingenieros, nuestro trabajo es alinear la tecnología con los requerimientos del producto. Elige la estrategia que maximice el valor para el usuario final.
# Arquitectura Escalable en Next.js 15: La Regla del Scope (/es/blog/scope-architecture-nextjs)
¿Alguna vez has entrado a un proyecto y te has sentido perdido en un mar de carpetas genéricas como `components`, `hooks` y `utils`?
Es un síntoma común de deuda técnica. A medida que una aplicación escala, la **ubicación** del código se vuelve tan crítica como la calidad del mismo. Cuando todo es "global", nada tiene un **dominio claro**.
La solución no es crear más carpetas. La solución es **arquitectura intencional** aplicada a través de **La Regla del Scope**.
La Regla del Scope: Contexto es Rey [#la-regla-del-scope-contexto-es-rey]
Este principio arquitectónico redefine cómo organizamos el código basándonos en su **alcance de uso**, no en su tipo técnico:
> **"La ubicación de un archivo está determinada por quién lo consume."**
En lugar de agrupar archivos por su extensión o "tipo" (todos los botones juntos, todos los hooks juntos), agrupamos por **funcionalidad de negocio** (Feature Slicing).
1. **Scope Local (1 Feature)**: Si un componente es exclusivo del Dashboard, vive **DENTRO** del directorio `dashboard`.
2. **Scope Compartido (2+ Features)**: Solo si un componente se utiliza en múltiples dominios (ej: Dashboard y Perfil), se promueve a `shared`.
***
Screaming Architecture en la Era de Next.js [#screaming-architecture-en-la-era-de-nextjs]
Tu estructura de carpetas debe "gritar" la intención del negocio, no el framework que utilizas. Esto facilita drásticamente el **onboarding** de nuevos desarrolladores.
**❌ Estructura Silenciosa (Legacy Pattern):**
```text
src/
components/ # ¿Botones? ¿Modales? ¿Cards de producto?
hooks/ # ¿Lógica de bóveda? ¿Auth?
pages/ # Rutas desconectadas de su lógica
```
**✅ Estructura que Grita (Scope Rule):**
```text
src/
app/
(auth)/ # Dominio: Autenticación
login/
_components/ # UI exclusiva de login (LoginForm)
(dashboard)/ # Dominio: Panel de Control
analytics/
_components/ # Gráficos de ventas (KPIChart)
shared/ # UI Kit base, primitivos reutilizables
```
**Impacto para el equipo:** Un desarrollador nuevo puede abrir el repo y entender **qué hace la aplicación** en segundos, sin navegar grafo de dependencias complejos.
***
Optimización y Server Components [#optimización-y-server-components]
En Next.js 15, esta arquitectura potencia el rendimiento y la separación de responsabilidades.
* **Server First Mentalily**: Al mantener componentes cerca de sus rutas (`app/dashboard/_components`), es natural escribirlos como Server Components, reduciendo el JS bundle enviado al cliente.
* **Colocación de Actions**: Tus `_actions.ts` viven junto al formulario que los invoca. Alta cohesión, bajo acoplamiento.
Caso de Estudio: Widget de Precios [#caso-de-estudio-widget-de-precios]
Imagina un `PriceWidget` complejo que solo existe en el Checkout.
* **Enfoque Tradicional**: Lo pones en `src/components/PriceWidget.tsx`. Contaminas el scope global con lógica de negocio específica.
* **Enfoque Scope Rule**: Vive en `src/app/(shop)/checkout/_components/price-widget.tsx`.
**Resultado**: Código modular. Si mañana eliminas la feature de checkout, eliminas su carpeta y el código muerto desaparece automáticamente. **Mantenibilidad garantizada.**
***
Checklist para una Arquitectura Robusta [#checklist-para-una-arquitectura-robusta]
Antes de crear un archivo, aplica este filtro de decisión:
1. 🛑 **¿Alcance?**
* 1 Feature → **Local** (`_components`, `_hooks`).
* 2+ Features → **Shared** (`shared/ui`).
2. ⚡ **¿Entorno?**
* Prioriza **Server Components** por defecto.
* Usa `'use client'` solo en las hojas del árbol de componentes (interactividad específica).
3. 📢 **¿Semántica?**
* Usa Route Groups `(auth)`, `(shop)` para organizar dominios sin afectar la URL.
***
La arquitectura no son reglas rígidas; es **comunicación**. Al adoptar la Regla del Scope, escribes código que explica su propio propósito, facilitando la vida a tu equipo actual y a los desarrolladores del futuro.
# Aliva Shop: Migración & Arquitectura Frontend (/es/experience/aliva-shop)
Tareas destacadas [#tareas-destacadas]
* **Estandarización de Código**: Implementación de BiomeJS para asegurar consistencia, linting estricto y formateo automático en todo el repositorio.
* **Ciclo de Integración Móvil**: Configuración y generación de builds de producción (APK/AAB) mediante Android Studio y Capacitor.
* **Implementación de Features**: Desarrollo ágil de nuevas funcionalidades de negocio solicitadas por el cliente, asegurando la escalabilidad.
* **Pixel-Perfect UI**: Traducción fiel de diseños de Figma a componentes de código reutilizables y responsivos.
* **Type Safety**: Introducción de tipado estricto en TypeScript para reducir errores en tiempo de ejecución y mejorar la DX.
* **Documentación Técnica**: Creación de documentación exhaustiva para facilitar el onboarding y mantenimiento futuro del código base.
Desafío Técnico: Modernización Legacy [#desafío-técnico-modernización-legacy]
El objetivo principal fue rescatar una base de código estancada y llevarla a estándares modernos para habilitar nuevas oportunidades de negocio.
Migración y Arquitectura [#migración-y-arquitectura]
* **Salto Tecnológico**: Ejecución de la migración de **Angular 15 a 20** y **Ionic 5 a 8**, eliminando deuda técnica bloqueante.
* **Reactive Programming**: Adopción de **Signals** para gestión de estado granular, reemplazando `Zone.js` donde fue posible para mejorar el rendimiento de detección de cambios.
Features Críticas [#features-críticas]
* **Geolocalización Precisa**: Integración nativa con Capacitor para tracking en tiempo real.
* **Push Notifications**: Sistema de notificaciones push.
Impacto en el Negocio [#impacto-en-el-negocio]
La refactorización no fue solo estética; desbloqueó la capacidad del equipo para iterar rápido. Lo que antes tomaba semanas en implementarse debido a la fragilidad del código legacy, ahora se despliega en días con confianza.
# Doctor Qali: Modernización de Frontend (/es/experience/doctor-qali)
Impacto Clave [#impacto-clave]
En un entorno ágil de HealthTech, mi rol superó la mera implementación de interfaces para enfocarse en **calidad de ingeniería**:
Modernización de Legacy Code [#modernización-de-legacy-code]
* **Refactorización Estratégica**: Migración progresiva de componentes de clase y anti-patrones hacia Functional Components y Hooks.
* **Type Safety**: Adopción de **TypeScript** para reducir bugs en runtime y mejorar la documentación viva del código.
Performance & UX [#performance--ux]
* **Component Library**: Implementación de **PrimeReact** para estandarizar la UI, reduciendo el tiempo de desarrollo de nuevas features en un **40%**.
* **Optimización de Carga**: Mejora de Core Web Vitals mediante code-splitting y memoización crítica.
Ingeniería de Producto [#ingeniería-de-producto]
Colaboración directa con producto para traducir requerimientos médicos complejos en interfaces intuitivas y accesibles para doctores y pacientes.
# Index (/es/experience)
Experiencia Profesional [#experiencia-profesional]
Mi perfil técnico se centra en la **arquitectura frontend escalable** y la **modernización de ecosistemas legacy**. Transformo deuda técnica en ventajas competitivas mediante stacks reactivos y patrones de diseño robustos.
# Tensolite: Desarrollo Full-Stack & Automatización (/es/experience/tensolite)
Ingeniería de Datos y Backend [#ingeniería-de-datos-y-backend]
* **Integración Masiva**: Desarrollo de pipelines ETL para la ingesta y validación de datos desde Excel hacia CRM corporativo, reduciendo errores humanos en un **90%**.
* **Automatización de Documentos**: Sistema de generación automática de reportes y PDFs técnicos, optimizando cientos de horas operativas mensuales.
* **Gestión de Estado Escablable**: Arquitectura robusta con **Redux y Zustand** para manejar flujos de datos complejos en aplicaciones de gestión interna.
* **Panel de Administración**: Desarrollo de módulos críticos para el Back-Office, habilitando el control total sobre el contenido y configuración de los portales corporativos.
Frontend y UX Corporativa [#frontend-y-ux-corporativa]
* **Diseño Pixel-Perfect**: Implementación fiel de interfaces Figma en componentes React reutilizables y modulares.
* **Gestión de Archivos**: Desarrollo de sistemas seguros para la carga, visualización y distribución de documentación técnica y planos.
Valor Aportado [#valor-aportado]
Más allá de la ejecución técnica, fomenté una **comunicación transversal** con el equipo de desarrollo. Participé activamente en la definición de requerimientos, impusé propuestas de valor tecnológico y lideré la resolución conjunta de problemas, promoviendo la adopción de herramientas que optimizaron costos y tiempos de entrega.
# Better Auth + MercadoPago: Pagos Type-Safe (/es/projects/better-auth-mp-plugin)
El Problema de la Integración de Pagos [#el-problema-de-la-integración-de-pagos]
Integrar pasarelas de pago suele introducir **deuda técnica inmediata**:
* **Webhooks "Caja Negra"**: Payloads sin tipar que rompen en runtime.
* **Lógica Dispersa**: Controladores de pago mezclados con lógica de negocio.
* **Desincronización**: El estado del usuario en la DB no coincide con el estado en la pasarela.
La Solución: Infraestructura como Código [#la-solución-infraestructura-como-código]
Este plugin no es solo un wrapper; es una pieza de **infraestructura** que estandariza cómo tu aplicación maneja el dinero.
Arquitectura Type-Safe [#arquitectura-type-safe]
Utilizando **Zod** y **TypeScript**, cada interacción con la API de MercadoPago está validada. Si el esquema de pago cambia, tu build falla antes de llegar a producción.
Principios de Diseño [#principios-de-diseño]
1. **Desacoplamiento Estricto**: La lógica de facturación vive aislada de la lógica de producto.
2. **Developer Experience (DX)**: Autocompletado inteligente para planes, suscripciones y eventos.
3. **Webhook Handler Unificado**: Un solo endpoint para gobernar todos los eventos de pago, con validación de firmas criptográficas automática.
Impacto Técnico [#impacto-técnico]
* **Reducción de Boilerplate**: Elimina +200 líneas de código repetitivo de integración.
* **Seguridad**: Validación automática de firmas y payloads.
* **Mantenibilidad**: Actualizaciones centralizadas al sdk de MercadoPago sin tocar el código de producto.
Diseñado para equipos que valoran la robustez y la seguridad en sus flujos de ingresos.
# Next.js 16: Cache Components Demo (/es/projects/cache-components-demo)
¿Por qué Cache Components? [#por-qué-cache-components]
En el desarrollo web moderno, raramente una página es 100% estática o 100% dinámica. Lo real es una mezcla compleja:
* **Nombre del Producto**: Cambia casi nunca (Estático).
* **Precio**: Cambia ocasionalmente (Revalidable).
* **Stock**: Cambia en tiempo real (Dinámico).
Hasta ahora, manejar esto requería estrategias complejas de segmentación o sacrificar rendimiento. **Next.js 16** introduce primitivas para resolver esto elegantemente.
Arquitectura de la Solución [#arquitectura-de-la-solución]
Este proyecto implementa una arquitectura de **Componentes Async Desacoplados**, donde cada componente es dueño de su propia estrategia de obtención y cacheo de datos.
```tsx
// ❌ Monolito de datos (Todo o nada)
const product = await db.getProduct(id);
// ✅ Cache Components (Granularidad)
// 'use cache' + cacheLife('weeks')
// 'use cache' + cacheTag('price')
// Dinámico + Streaming
```
Features Implementadas [#features-implementadas]
1. **Cacheo Granular**: Control independiente por componente usando la directiva `'use cache'`.
2. **Revalidación por Tags**: Sistema de invalidación precisa. Un cambio de precio no obliga a re-renderizar la descripción del producto.
3. **Static Shell + Streaming**: El HTML base se sirve instantáneamente (como SSG), mientras que los datos vivos (Stock) entran vía streaming (PPR).
4. **Optimización Automática**: Next.js paraleliza las promesas automáticamente sin necesidad de `Promise.all` manual.
Valor Educativo [#valor-educativo]
Este repositorio sirve como un **campo de pruebas** para entender el futuro del data-fetching en React. Demuestra patrones correctos de `Suspense`, manejo de errores en Server Components y la integración de Zod para validar inputs en Server Actions.
Es una referencia viva para equipos que planean migrar a Next.js 16 y adoptar PPR (Partial Prerendering).
# Proyectos e Ingeniería de Software (/es/projects)
Ingeniería Aplicada [#ingeniería-aplicada]
Colección de desarrollos donde exploro patrones de arquitectura avanzados, seguridad y performance a escala.
# Lules Market: E-commerce Hiperlocal (/es/projects/lules-market)
Visión del Producto: Digitalizando el Barrio [#visión-del-producto-digitalizando-el-barrio]
Lules Market nace con una misión clara: **democratizar el acceso al e-commerce** para pequeños comerciantes que no pueden costear desarrollos a medida.
No es solo una tienda, es un **ecosistema SaaS** donde cada comercio gestiona su inventario, ventas y suscripciones de forma autónoma.
Arquitectura para Escalar [#arquitectura-para-escalar]
El desafío técnico fue construir un sistema que se sienta simple para el usuario ("Abuela-proof") pero que sea tecnológicamente avanzado bajo el capó.
Backend & Datos [#backend--datos]
* **ElysiaJS (Bun)**: Elegido por su performance superior y baja latencia en el manejo de API REST.
* **SQL-First con Drizzle**: Consultas a base de datos eficientes, type-safe y sin la overhead de ORMs tradicionales. PostgreSQL asegura la integridad referencial de miles de productos y transacciones.
Frontend & UX [#frontend--ux]
* **Optimizistic UI**: Las interacciones (likes, carrito, suscripción) se sienten instantáneas.
* **Accesibilidad**: Componentes de `shadcn/ui` garantizan que la plataforma sea usable por todos.
Infraestructura de Pagos [#infraestructura-de-pagos]
Integración profunda con **MercadoPago** para manejar el ciclo de vida financiero del comercio:
1. **Onboarding Automatizado**: Suscripción de comercios al plan mensual.
2. **Webhooks Resilientes**: Sistema a prueba de fallos para procesar confirmaciones de pago incluso con intermitencias de red.
3. **Seguridad**: Validación de firmas y tokens para prevenir fraudes.
Impacto [#impacto]
Lules Market demuestra que la tecnología de punta no es solo para startups de Silicon Valley; es una herramienta poderosa para **reactivar economías locales** con software de calidad global.
# MemesDev: Red Social para Developers (/es/projects/memes-dev)
Más que un Side Project [#más-que-un-side-project]
MemesDev no es solo una galería de imágenes; es una **aplicación social full-stack** diseñada bajo los principios de producción.
El objetivo fue crear una **Arquitectura de Referencia** para la comunidad, demostrando cómo integrar piezas complejas del ecosistema moderno de React.
Stack Tecnológico de Punta [#stack-tecnológico-de-punta]
La aplicación orquesta múltiples capas de infraestructura moderna:
* **Autenticación Robusta**: Implementación de **Better Auth** (Magic Links + OAuth) para una experiencia de login sin fricción y segura.
* **Gestión de Datos**: Modelado de relaciones complejas (Usuarios, Posts, Comments, Likes) con **Prisma ORM** y **PostgreSQL**.
* **Validación End-to-End**: **Zod** garantiza que los datos sean correctos desde el formulario en el cliente hasta la base de datos.
* **UX Instantánea**: Uso de **Optimistic Updates** con TanStack Query/Form para que la interacción se sienta nativa (zero-latency feel).
Filosofía Open Source [#filosofía-open-source]
El código es público y documentado param servir como recurso educativo:
1. **Clean Code**: Estructura de carpetas escalable.
2. **Linting Estricto**: Configuración de **BiomeJS** para mantener calidad de código.
3. **Accesibilidad**: Componentes de `shadcn/ui` para garantizar inclusión.
Resultado [#resultado]
Una plataforma viva donde convergen el humor y la ingeniería de software de alta calidad.
# Pikuu: Generador de Backend & UI con AI (/es/projects/pikuu)
La Evolución del Code Gen [#la-evolución-del-code-gen]
Pikuu no es otro generador de código genérico que escupe archivos sin contexto. Es un **asistente técnico** diseñado para integrarse en el flujo de trabajo de un desarrollador senior.
A diferencia de herramientas que intentan "hacer todo" de una vez, Pikuu adopta un enfoque **iterativo y granular**:
* Entiende el contexto global del proyecto.
* Modifica solo lo que le pides.
* Mantiene el estado entre iteraciones.
Features de Ingeniería [#features-de-ingeniería]
El núcleo de Pikuu está construido para respetar las buenas prácticas de arquitectura moderna en Next.js:
* **Schema-First Design**: Genera `schema.prisma` optimizados desde una idea abstracta o consume esquemas existentes.
* **Seguridad de Tipado**: Crea esquemas de validación **Zod** sincronizados con tu base de datos.
* **Backend Robusto**: Escribe **Server Actions** o Route Handlers puros, asegurando que nunca se mezcle lógica de servidor y cliente.
* **Forms de Producción**: Genera formularios complejos usando **shadcn/ui** integrados con **TanStack Form** para manejo de estado y validación.
Filosofía: No Placeholders [#filosofía-no-placeholders]
La regla de oro de Pikuu es: **"Si no corre en producción, no sirve"**.
1. **Iteración Controlada**: Nada de regenerar todo el proyecto por un cambio pequeño.
2. **Streaming UI**: Visualiza los cambios en tiempo real mediante artefactos de UI generados al vuelo (Generative UI).
3. **Strict TypeScript**: Todo el código generado pasa los chequeos más estrictos de TypeScript.
Stack Tecnológico [#stack-tecnológico]
La herramienta utiliza lo último del ecosistema de Vercel y React:
* **Motor AI**: Vercel AI SDK v6 + AI Elements para streaming de componentes.
* **Base de Datos**: PostgreSQL + Prisma ORM.
* **Interfaz**: Framer Motion para transiciones fluidas y shadcn/ui.
Licencia [#licencia]
Este es un proyecto **Source-Available**. El código fuente está disponible para fines educativos y de auditoría, pero su uso comercial, redistribución o modificación para servicios derivados está restringida sin permiso explícito.
¿Te interesa implementar Pikuu en tu equipo? [Hablemos](/).
# Beneficios y costes (/es/blog/cache-components/benefits)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Cache Components no solo mejora UX; también puede bajar costes operativos cuando reduces trabajo en request time.
Qué se ahorra realmente [#qué-se-ahorra-realmente]
En arquitectura granular, el ahorro principal suele venir de:
* Menos queries en request time
* Menos CPU de render por request
* Menor presión sobre picos de tráfico
* Mejor cache hit ratio en contenido estable
El objetivo no es "cero queries", sino mover trabajo repetitivo de request
time hacia contenido cacheado y controlado por `cacheLife` + tags.
Modelo mental simple [#modelo-mental-simple]
Piensa en dos capas:
1. **Trabajo en build/revalidación**: costo amortizado
2. **Trabajo por request**: costo variable (escala con tráfico)
Cuando el tráfico crece, el costo variable domina. Por eso reducir trabajo por request suele impactar más en coste final.
Comparación rápida [#comparación-rápida]
```tsx
async function ProductPage({ id }) {
const product = await db.query('SELECT * FROM products WHERE id = ?', [id])
return
}
```
* 1 query grande por request
* Render completo por request
* Menor complejidad inicial
```tsx
export default function ProductPage({ params }) {
return (
}>
)
}
async function ProductText({ id }) {
'use cache'
cacheTag(`product-text-${id}`)
cacheLife('weeks')
return db.getProductText(id)
}
async function ProductPrice({ id }) {
'use cache'
cacheTag(`product-price-${id}`)
cacheLife('hours')
return db.getProductPrice(id)
}
async function ProductStock({ id }) {
return db.getProductStock(id)
}
```
* Texto y precio cacheados
* Solo stock queda siempre fresh
* Más control sobre qué invalida y cuándo
Fórmulas útiles para estimar ahorro [#fórmulas-útiles-para-estimar-ahorro]
Si defines:
* `R` = requests por día
* `Q_all` = queries/request en enfoque tradicional
* `Q_rt` = queries/request que quedan sin cache (request time)
Entonces:
* **Queries request-time tradicionales** = `R * Q_all`
* **Queries request-time granulares** = `R * Q_rt`
* **Ahorro request-time** = `R * (Q_all - Q_rt)`
Ejemplo numérico [#ejemplo-numérico]
* `R = 1,000,000`
* Tradicional: `Q_all = 1` (query completa)
* Granular: `Q_rt = 1` (solo stock)
En este ejemplo no bajas cantidad de queries/request, pero sí:
* Bajas tamaño y costo de la query request-time
* Sirves parte del HTML ya resuelto
* Mejoras TTFB percibido por static shell + streaming
Si en tu caso tradicional tenía varias queries request-time y en granular dejas solo una pequeña, el ahorro crece de forma significativa.
Beneficio de negocio (educativo) [#beneficio-de-negocio-educativo]
Para explicar ahorro en una demo, habla en términos de:
1. **Costo variable por request** (DB + CPU)
2. **Costo amortizado por revalidación**
3. **Latencia percibida** (usuario ve contenido útil antes)
En productos con alto tráfico y contenido mayormente estable, el patrón
granular suele reducir costo variable y mejorar experiencia al mismo tiempo.
Dónde puede salir más caro [#dónde-puede-salir-más-caro]
No todo debe cachearse granularmente. Puede empeorar si:
* Fragmentas en demasiados componentes sin necesidad
* Diseñas tags ambiguos o difíciles de invalidar
* Revalidas en exceso por eventos muy frecuentes
Regla práctica [#regla-práctica]
* Cachea granular solo campos con volatilidad distinta
* Usa el perfil de `cacheLife` más largo aceptable
* Prefiere invalidación por tags específicos
Checklist de implementación [#checklist-de-implementación]
* [ ] `cacheComponents: true` habilitado
* [ ] Componentes cacheados con `'use cache'`
* [ ] `cacheTag` por unidad funcional (ej. `product-price-${id}`)
* [ ] `revalidateTag` / `updateTag` según consistencia requerida
* [ ] Runtime data dentro de `Suspense`
* [ ] Medición con logs y headers (`x-nextjs-cache`)
Qué medir en producción [#qué-medir-en-producción]
Métricas mínimas recomendadas:
* P95/P99 de latencia en páginas críticas
* Ratio HIT/MISS/STALE por ruta
* Queries por request (promedio y percentiles)
* CPU por request en SSR
Con esas 4 métricas puedes justificar ahorro de costos con datos, no solo con teoría.
# Conceptos Clave (/es/blog/cache-components/concepts)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
1. Componente async = Promesa [#1-componente-async--promesa]
El concepto más importante para entender:
```tsx
// Este componente async RETORNA una promesa
async function ProductStock({ productId }) {
const stock = await db.getStock(productId);
return
{stock}
;
}
// Por eso el Suspense va en el PADRE:
function Parent() {
return (
{/* ← Esta línea CREA y EJECUTA la promesa */}
);
}
```
Cuando React renderiza ``, está invocando una función async, lo cual crea una promesa. El Suspense en el padre **captura** esa promesa.
**Error común:** Intentar poner Suspense DENTRO del componente async
❌ Incorrecto [#-incorrecto]
```tsx
async function ProductStock() {
return (
{/* Ya pasó el await, no sirve */}
{await db.query(...)}
)
}
```
**Por qué no funciona:**
* El `await` se ejecuta ANTES del `return`
* Cuando llegas al `return`, la promesa ya se resolvió
* El Suspense no tiene nada que capturar
✅ Correcto [#-correcto]
```tsx
function Parent() {
return (
{/* Suspense captura la ejecución del componente */}
)
}
async function ProductStock() {
const stock = await db.query(...)
return
{stock}
}
```
2. Múltiples queries vs Cache [#2-múltiples-queries-vs-cache]
La pregunta común [#la-pregunta-común]
> "¿No es ineficiente hacer 3 queries separadas por el mismo producto?"
**Respuesta:** Depende de cuándo se ejecutan.
Modelo tradicional [#modelo-tradicional]
```tsx
// Una query en cada request
async function ProductPage({ productId }) {
const product = await db.query('SELECT * FROM products')
return (
{product.name}
${product.price}
Stock: {product.stock}
)
}
```
**Timeline:**
* Request 1: Query completa
* Request 2: Query completa
* Request 3: Query completa
**Total:** 3 queries a la DB
Modelo granular [#modelo-granular]
```tsx
// 3 queries, pero solo 1 en request time
function ProductPage({ productId }) {
return (
<>
{/* Build time */}
{/* Build time */}
{/* Request time */}
>
)
}
```
**Timeline:**
* Build: 2 queries (texto + precio)
* Request 1: 1 query (stock)
* Request 2: 1 query (stock)
* Request 3: 1 query (stock)
**Total:** 2 queries en build + 3 queries en request = 5 queries
Pero cada request solo ejecuta 1 query, y el HTML viene pre-generado.
Trade-off consciente [#trade-off-consciente]
| Aspecto | Query única | Queries separadas |
| --------------------- | ----------------------- | ----------------------------- |
| Queries totales | 3 (1 por request) | 5 (2 en build + 3 en request) |
| Queries por request | 1 query completa | 1 query pequeña (stock) |
| Control granular | ❌ Todo o nada | ✅ Por campo |
| Performance percibida | Depende del cache total | HTML instantáneo + streaming |
| Complejidad | Baja | Media |
**Conclusión:** Más queries totales, pero mejor experiencia de usuario y control fino.
3. Tags para revalidación [#3-tags-para-revalidación]
Los tags permiten invalidar cache de forma **selectiva** y **precisa**.
Sin tags (tradicional) [#sin-tags-tradicional]
```tsx
// Invalidar toda la página
revalidatePath("/product/1");
// Invalida TODO el producto:
// - Texto (que rara vez cambia)
// - Precio (el único que cambió)
// - Stock (que ya no tiene cache)
```
Con tags (granular) [#con-tags-granular]
```tsx
// Cachear con tags únicos
async function ProductPrice({ productId }) {
"use cache";
cacheTag(`product-price-${productId}`);
// ...
}
// Invalidar solo el precio
revalidateTag(`product-price-${productId}`, "max");
// El texto mantiene su cache ✅
// El stock ya no tiene cache ✅
```
Anatomía de un tag [#anatomía-de-un-tag]
```tsx
cacheTag(`product-price-${productId}`);
// Tag final: "product-price-1"
// Permite: revalidateTag('product-price-1', 'max')
```
**Ventaja:** Revalidación quirúrgica sin afectar otros campos.
4. Runtime data requiere Suspense [#4-runtime-data-requiere-suspense]
Runtime data es información que solo existe cuando llega una request:
* `params` - Parámetros de la URL
* `searchParams` - Query strings
* `cookies()` - Cookies del usuario
* `headers()` - Headers de la request
Por qué necesitan Suspense [#por-qué-necesitan-suspense]
```tsx
// ❌ Esto da error
export default async function ProductPage({ params }) {
const { id } = await params; // Runtime data sin Suspense
return
{id}
;
}
// Error: Runtime data was accessed outside of
```
**Razón:** Durante prerendering (build time), `params` no existe. Next.js requiere que marques explícitamente que este contenido necesita request context.
Solución [#solución]
```tsx
// ✅ Correcto
export default function ProductPage({ params }) {
return (
}>
{/* params pasa como prop */}
)
}
async function ProductContent({ params }) {
const { id } = await params {/* Ahora SÍ está en Suspense */}
return
{id}
}
```
El patrón es: componente sync pasa runtime data como prop a componente async
dentro de Suspense.
5. Static shell vs Streaming [#5-static-shell-vs-streaming]
Static shell [#static-shell]
Contenido que se prerrenderiza y se incluye en el HTML inicial:
```tsx
// Estos componentes con 'use cache' generan static shell
// → HTML incluido
// → HTML incluido
```
**Ventaja:** El usuario ve contenido instantáneamente, sin esperar queries.
Streaming [#streaming]
Contenido que se renderiza en request time y hace streaming al browser:
```tsx
// Este componente sin 'use cache' hace streaming
}>
// → Streaming después
```
**Ventaja:** Contenido siempre fresh, sin bloquear el HTML inicial.
Visualización [#visualización]
```
User Request → Server
↓
[Prerendering check]
↓
┌──────────┴──────────┐
↓ ↓
Static Shell Streaming
(instant) (on-demand)
↓ ↓
Browser ←────────────────┘
↓
[Shows static + skeleton]
↓
[Streaming replaces skeleton]
↓
[Complete page rendered]
```
6. cacheLife profiles [#6-cachelife-profiles]
Perfiles predefinidos para diferentes tipos de contenido:
```tsx
cacheLife("default"); // Balance general
cacheLife("minutes"); // Volátil (ej: trending topics)
cacheLife("hours"); // Cambia ocasionalmente (ej: precios)
cacheLife("days"); // Cambia raramente (ej: categorías)
cacheLife("weeks"); // Muy estable (ej: términos legales)
cacheLife("max"); // Casi nunca cambia (ej: contenido histórico)
```
O define tu propio perfil:
```tsx
cacheLife({
stale: 3600, // 1 hora hasta considerarse stale
revalidate: 7200, // 2 horas hasta revalidar
expire: 86400, // 1 día hasta expirar completamente
});
```
**Regla práctica:** Usa el perfil más largo que sea aceptable para tu caso de
uso. Más cache = mejor performance.
# Implementación (/es/blog/cache-components/implementation)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
import { Step, Steps } from "fumadocs-ui/components/steps";
Habilitar Cache Components [#habilitar-cache-components]
Actualiza `next.config.mjs`:
```js title="next.config.mjs"
const config = {
cacheComponents: true, // ← Habilita Cache Components
};
export default config;
```
Este flag habilita **Partial Prerendering (PPR)** automáticamente.
Generar rutas estáticas del segmento dinámico [#generar-rutas-estáticas-del-segmento-dinámico]
Para que Next.js conozca qué productos prerenderizar para `/product/[id]`, agrega `generateStaticParams`:
```tsx title="app/product/[id]/page.tsx"
import { db } from "@/lib/db";
export async function generateStaticParams() {
const products = await db.listProducts();
return products.map((product) => ({
id: product.id,
}));
}
```
Sin `generateStaticParams`, la ruta dinámica sigue funcionando, pero pierdes
parte del valor educativo de ver claramente qué paths se prerenderizan.
Crear queries separadas [#crear-queries-separadas]
Cada campo necesita su propia query:
```ts title="lib/db.ts"
export const db = {
// Query SOLO para texto
async getProductText(productId: string) {
console.log(`[DB Query] 📝 getProductText - Product ${productId}`);
const { name, description } = await db.query(
"SELECT name, description FROM products WHERE id = ?",
[productId]
);
return { name, description };
},
// Query SOLO para precio
async getProductPrice(productId: string) {
console.log(`[DB Query] 💰 getProductPrice - Product ${productId}`);
const { price } = await db.query(
"SELECT price FROM products WHERE id = ?",
[productId]
);
return { price };
},
// Query SOLO para stock
async getProductStock(productId: string) {
console.log(`[DB Query] 📦 getProductStock - Product ${productId}`);
const { stock } = await db.query(
"SELECT stock FROM products WHERE id = ?",
[productId]
);
return { stock, lastChecked: new Date().toISOString() };
},
};
```
**¿Por qué múltiples queries?** Aunque parece ineficiente, las queries
cacheadas se ejecutan en **build time**, no en cada request.
Componente de texto (cacheado) [#componente-de-texto-cacheado]
```tsx title="app/product/[id]/_components/product-text.tsx"
async function ProductText({ productId }: { productId: string }) {
"use cache";
cacheTag(`product-text-${productId}`);
cacheLife("weeks"); // Cache largo: rara vez cambia
const { name, description } = await db.getProductText(productId);
return (
{name}
📦 Cacheado 1 semana
{description}
);
}
```
**Puntos clave:**
* `'use cache'` marca el componente como cacheable
* `cacheTag` permite revalidación selectiva
* `cacheLife('weeks')` define duración del cache
* Query específica solo para este campo
Componente de precio (cacheado) [#componente-de-precio-cacheado]
```tsx title="app/product/[id]/_components/product-price.tsx"
async function ProductPrice({ productId }: { productId: string }) {
"use cache";
cacheTag(`product-price-${productId}`);
cacheLife("hours"); // Cache medio: cambia ocasionalmente
const { price } = await db.getProductPrice(productId);
return (
Precio${price.toFixed(2)}
⏱️ Cacheado 1 hora
);
}
```
Componente de stock (sin cache) [#componente-de-stock-sin-cache]
```tsx title="app/product/[id]/_components/product-stock.tsx"
async function ProductStock({ productId }: { productId: string }) {
// Sin 'use cache' - siempre fresh
const { stock, lastChecked } = await db.getProductStock(productId);
return (
);
}
```
**Sin `use cache`** = el componente se ejecuta en cada request y hace
streaming.
Página principal con Suspense [#página-principal-con-suspense]
```tsx title="app/product/[id]/page.tsx"
import { Suspense } from "react";
// Componente principal - SYNC, solo estructura
export default function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
return (
← Volver
{/* Todo el contenido dinámico en Suspense */}
}>
{/* Texto y precio: cacheados, van al static shell */}
{/* Stock: sin cache, requiere Suspense adicional */}
}>
);
}
```
**Crítico:** `params` es runtime data y debe accederse dentro de Suspense.
Server Actions para revalidación [#server-actions-para-revalidación]
```ts title="app/actions.ts"
"use server";
import { revalidateTag } from "next/cache";
import { z } from "zod";
const productIdSchema = z
.string()
.trim()
.min(1)
.regex(/^[a-zA-Z0-9-]+$/);
function validateProductId(productId: string) {
return productIdSchema.parse(productId);
}
export async function revalidateProductPrice(productId: string) {
const safeProductId = validateProductId(productId);
console.log(`[Server Action] Revalidating price for ${safeProductId}`);
revalidateTag(`product-price-${safeProductId}`, "max");
return { success: true, message: "Precio revalidado" };
}
export async function revalidateProductText(productId: string) {
const safeProductId = validateProductId(productId);
console.log(`[Server Action] Revalidating text for ${safeProductId}`);
revalidateTag(`product-text-${safeProductId}`, "max");
return { success: true, message: "Texto revalidado" };
}
```
Timeline de ejecución [#timeline-de-ejecución]
```
pnpm build
✅ generateStaticParams lista ids estáticos de producto
✅ ProductText ejecuta query → resultado en static shell
✅ ProductPrice ejecuta query → resultado en static shell
❌ ProductStock NO se ejecuta (requiere request)
Output:
- HTML con texto y precio ya renderizados
- Placeholder para stock (Suspense fallback)
```
```
Usuario visita /product/1
⚡ Browser recibe HTML inmediatamente
- Texto: "Laptop Dell XPS 15" (del static shell)
- Precio: "$1,299.99" (del static shell)
- Stock: (placeholder)
🔄 Server ejecuta ProductStock
- Query a DB para stock actual
⚡ Stock hace streaming al browser
- Reemplaza
- Usuario ve: "15 unidades"
```
```
Usuario hace click en "Revalidar Precio"
🔄 revalidateProductPrice() ejecuta
- Marca 'product-price-1' como stale
📝 Próxima visita a /product/1:
- Texto: Sigue del cache (no revalidado)
- Precio: Se regenera en background
- Stock: Se ejecuta fresh (sin cache)
```
Verificación [#verificación]
En desarrollo [#en-desarrollo]
```bash
pnpm dev
```
Abre [http://localhost:3000](http://localhost:3000) y verifica en consola del servidor:
```
[DB Query] 📝 getProductText - Product 1
[DB Query] 💰 getProductPrice - Product 1
[DB Query] 📦 getProductStock - Product 1
```
En producción [#en-producción]
```bash
pnpm build
pnpm start
```
Verifica en el output del build:
```
Route (app) Size First Load JS
┌ ◐ / 2.4 kB 170 kB
└ ◐ /product/[id]
◐ Partial Prerender - static HTML + contenido dinámico por streaming
```
Si ves `◐ Partial Prerender` para `/product/[id]`, está funcionando
correctamente para este demo de Cache Components.
Debugging [#debugging]
Ver qué se cachea [#ver-qué-se-cachea]
En el browser, haz View Source (Ctrl+U) y busca:
```html
Laptop Dell XPS 15
Potente laptop con procesador...
$1,299.99
```
Logs útiles [#logs-útiles]
Agrega logs en cada componente:
```tsx
async function ProductPrice({ productId }) {
"use cache";
console.log(`[${new Date().toISOString()}] Rendering ProductPrice`);
// ...
}
```
En build, deberías ver el log una sola vez.
En request time, NO deberías verlo (usa cache).
# Cache Components (/es/blog/cache-components)
import { Callout } from "fumadocs-ui/components/callout";
import { Card, Cards } from "fumadocs-ui/components/card";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Demo interactivo de cómo cachear **campos individuales** de un registro con
diferentes estrategias en Next.js 16, mejorando latencia percibida y
reduciendo carga en infraestructura durante request time.
El Problema [#el-problema]
Imagina que tienes un **producto** con 3 campos:
* **Texto** (nombre + descripción): Rara vez cambia
* **Precio**: Cambia ocasionalmente
* **Stock**: Debe estar siempre actualizado
¿Cómo cacheas cada campo de forma independiente?
❌ Lo que NO funciona [#-lo-que-no-funciona]
```tsx
// Intentar cachear selectivamente desde un solo objeto
async function ProductPage({ productId }) {
// Una sola query trae todo
const product = await db.query("SELECT * FROM products WHERE id = ?");
// ¿Cómo cacheo solo el precio?
// ¿Cómo NO cacheo el stock?
return (
);
}
```
**No funciona porque:** Una vez que tienes el objeto completo, no puedes aplicar `use cache` selectivamente a campos individuales.
✅ La Solución [#-la-solución]
Cada campo es un **componente async separado** con su propia query y estrategia de cache.
```tsx
export default function ProductPage({ params }) {
return (
}>
);
}
async function ProductContent({ params }) {
const { id } = await params;
return (
{/* CAMPO 1: Texto cacheado */}
{/* CAMPO 2: Precio cacheado */}
{/* CAMPO 3: Stock sin cache */}
}>
);
}
```
**Importante:** El Suspense va en el **padre**, no dentro del componente
async.
Arquitectura [#arquitectura]
```
ProductPage (sync - estático)
│
└─
└─ ProductContent (async - accede a params)
├─ ProductText (async + 'use cache')
├─ ProductPrice (async + 'use cache')
└─
└─ ProductStock (async sin cache)
```
Comparación Visual [#comparación-visual]
En build time, Next.js prerrenderiza la página: - ✅ `ProductText` ejecuta
query → incluido en static shell - ✅ `ProductPrice` ejecuta query →
incluido en static shell - ❌ `ProductStock` NO se ejecuta (requiere request
context) **Resultado:** HTML con texto y precio ya renderizados
Cuando un usuario visita la página: 1. ⚡ Browser recibe HTML
instantáneamente (texto + precio) 2. ⚡ Browser muestra skeleton del stock
3\. 🔄 Server ejecuta query de stock 4. ⚡ Stock hace streaming y reemplaza
skeleton
Siguientes pasos [#siguientes-pasos]
# Revalidación (/es/blog/cache-components/revalidation)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Next.js 16 ofrece dos formas de invalidar cache: `revalidateTag` y `updateTag`. Ambos funcionan con tags, pero tienen comportamientos diferentes.
updateTag vs revalidateTag [#updatetag-vs-revalidatetag]
Stale-while-revalidate [#stale-while-revalidate]
```ts title="app/actions.ts"
'use server'
import { revalidateTag } from 'next/cache'
export async function revalidatePrice(productId: string) {
revalidateTag(`product-price-${productId}`, 'max')
// ↑
// Perfil de revalidación
}
```
Comportamiento [#comportamiento]
1. **Marca el cache como "stale"** (obsoleto)
2. **Sigue sirviendo contenido viejo** mientras regenera en background
3. **Próxima request** obtiene contenido nuevo
Timeline [#timeline]
```
t=0s User 1 clicks "Revalidar"
→ Cache marcado como stale
→ Sigue sirviendo versión vieja
t=1s User 2 visita página
→ Recibe versión vieja
→ Trigger regeneración en background
t=3s Regeneración completa
t=5s User 3 visita página
→ Recibe versión NUEVA ✨
```
Perfiles disponibles [#perfiles-disponibles]
```ts
revalidateTag('my-tag', 'max') // Más agresivo
revalidateTag('my-tag', 'default') // Balance
revalidateTag('my-tag', 'min') // Más conservador
// O custom
revalidateTag('my-tag', {
stale: 3600,
revalidate: 7200,
expire: 86400
})
```
Usa cuando [#usa-cuando]
✅ El cambio no es crítico inmediato\
✅ Puedes tolerar data vieja brevemente\
✅ Blog posts, precios, contenido editorial\
✅ Quieres minimizar latencia para usuarios
Ejemplo real [#ejemplo-real]
```ts title="app/actions.ts"
'use server'
import { revalidateTag } from 'next/cache'
// Actualizar precio de producto
export async function updateProductPrice(
productId: string,
newPrice: number
) {
// 1. Actualizar en DB
await db.query(
'UPDATE products SET price = ? WHERE id = ?',
[newPrice, productId]
)
// 2. Revalidar cache (stale-while-revalidate)
revalidateTag(`product-price-${productId}`, 'max')
return { success: true }
}
```
Invalidación inmediata [#invalidación-inmediata]
```ts title="app/actions.ts"
'use server'
import { updateTag } from 'next/cache'
export async function updateCartItem(userId: string) {
updateTag(`user-cart-${userId}`)
// No requiere segundo argumento
}
```
Comportamiento [#comportamiento-1]
1. **Expira el cache inmediatamente**
2. **Siguiente request regenera** el contenido
3. **Usuario obtiene contenido fresco** en la misma request
Timeline [#timeline-1]
```
t=0s User 1 clicks "Agregar al carrito"
→ Cache EXPIRA inmediatamente
t=1s User 2 visita página
→ Cache expirado, regenera en request
→ Recibe versión NUEVA ✨
t=2s User 3 visita página
→ Recibe versión NUEVA ✨
```
Usa cuando [#usa-cuando-1]
✅ El cambio debe ser inmediato\
✅ No puedes tolerar data vieja\
✅ Carrito de compras\
✅ Likes, votos, reacciones\
✅ Mutaciones de usuario
Ejemplo real [#ejemplo-real-1]
```ts title="app/actions.ts"
'use server'
import { updateTag } from 'next/cache'
// Agregar item al carrito
export async function addToCart(
userId: string,
productId: string
) {
// 1. Agregar a DB
await db.query(
'INSERT INTO cart_items (user_id, product_id) VALUES (?, ?)',
[userId, productId]
)
// 2. Invalidar cache inmediatamente
updateTag(`user-cart-${userId}`)
return { success: true }
}
```
Comparación directa [#comparación-directa]
| Aspecto | revalidateTag | updateTag |
| --------------------- | -------------------------- | --------------------------- |
| **Comportamiento** | Stale-while-revalidate | Invalidación inmediata |
| **Latencia** | Baja (sirve viejo primero) | Media (regenera en request) |
| **Consistencia** | Eventual | Inmediata |
| **Performance** | Mejor | Buena |
| **Caso de uso** | Contenido editorial | Mutaciones de usuario |
| **Segundo argumento** | Sí (perfil) | No |
Ejemplos de casos de uso [#ejemplos-de-casos-de-uso]
Blog / Contenido editorial [#blog--contenido-editorial]
```ts title="app/actions.ts"
"use server";
import { revalidateTag } from "next/cache";
export async function publishBlogPost(postId: string) {
await db.query("UPDATE posts SET published = true WHERE id = ?", [postId]);
// Stale-while-revalidate: OK que algunos vean versión vieja
revalidateTag(`blog-post-${postId}`, "max");
revalidateTag("blog-list", "max");
}
```
E-commerce / Carrito [#e-commerce--carrito]
```ts title="app/actions.ts"
"use server";
import { updateTag } from "next/cache";
export async function addToCart(userId: string, productId: string) {
await db.query("INSERT INTO cart_items (user_id, product_id) VALUES (?, ?)", [
userId,
productId,
]);
// Invalidación inmediata: usuario debe ver su carrito actualizado YA
updateTag(`user-cart-${userId}`);
}
```
Sistema de likes [#sistema-de-likes]
```ts title="app/actions.ts"
"use server";
import { updateTag } from "next/cache";
export async function likePost(userId: string, postId: string) {
await db.query("INSERT INTO likes (user_id, post_id) VALUES (?, ?)", [
userId,
postId,
]);
// Invalidación inmediata: usuario debe ver su like reflejado
updateTag(`post-likes-${postId}`);
updateTag(`user-liked-posts-${userId}`);
}
```
Precios de productos [#precios-de-productos]
```ts title="app/actions.ts"
"use server";
import { revalidateTag } from "next/cache";
export async function updatePrice(productId: string, newPrice: number) {
await db.query("UPDATE products SET price = ? WHERE id = ?", [
newPrice,
productId,
]);
// Stale-while-revalidate: OK que el precio tarde un poco en actualizarse
revalidateTag(`product-price-${productId}`, "max");
}
```
Revalidar múltiples tags [#revalidar-múltiples-tags]
Ambas funciones se pueden llamar múltiples veces:
```ts title="app/actions.ts"
"use server";
import { revalidateTag, updateTag } from "next/cache";
export async function updateProduct(productId: string, data: ProductData) {
await db.query("UPDATE products SET ... WHERE id = ?", [productId]);
// Combinar ambos enfoques según necesidad
revalidateTag(`product-text-${productId}`, "max"); // Texto: stale-while-revalidate
revalidateTag(`product-price-${productId}`, "max"); // Precio: stale-while-revalidate
updateTag(`product-inventory-${productId}`); // Inventario: inmediato
}
```
revalidatePath vs Tags [#revalidatepath-vs-tags]
revalidatePath (tradicional) [#revalidatepath-tradicional]
```ts
revalidatePath("/product/1");
```
**Invalida:** Toda la ruta `/product/1`
**Problema:** No puedes invalidar campos específicos
Tags (granular) [#tags-granular]
```ts
revalidateTag("product-price-1", "max");
```
**Invalida:** Solo el cache del precio del producto 1
**Ventaja:** Quirúrgico, no afecta otros campos
**Regla general:** Usa tags para control granular. Solo usa `revalidatePath`
cuando quieras invalidar toda una página.
Debugging revalidación [#debugging-revalidación]
Logs útiles [#logs-útiles]
```ts title="app/actions.ts"
"use server";
import { revalidateTag } from "next/cache";
export async function revalidatePrice(productId: string) {
console.log(
`[Revalidation] product-price-${productId} at ${new Date().toISOString()}`
);
revalidateTag(`product-price-${productId}`, "max");
}
```
Verificar en browser [#verificar-en-browser]
1. Click en "Revalidar"
2. Refresh la página
3. View Source (Ctrl+U)
4. Busca el contenido actualizado en el HTML
Si el contenido cambió, la revalidación funcionó.
Headers de respuesta [#headers-de-respuesta]
Next.js incluye headers útiles:
```
x-nextjs-cache: HIT | MISS | STALE
```
* `HIT` - Contenido del cache
* `MISS` - Regenerado
* `STALE` - Stale-while-revalidate en progreso
Mejores prácticas [#mejores-prácticas]
1. Usa el approach correcto [#1-usa-el-approach-correcto]
```ts
// ✅ Contenido editorial
revalidateTag("blog-post", "max");
// ✅ Mutaciones de usuario
updateTag("user-cart");
```
2. Tags descriptivos [#2-tags-descriptivos]
```ts
// ✅ Bueno
cacheTag(`product-price-${productId}`);
cacheTag(`user-profile-${userId}`);
// ❌ Malo
cacheTag("data", id);
cacheTag("cache1", value);
```
3. Revalidar relacionados [#3-revalidar-relacionados]
```ts
// Actualizar producto → revalidar múltiples tags
revalidateTag(`product-${productId}`, "max");
revalidateTag("product-list", "max");
revalidateTag(`category-${categoryId}`, "max");
```
4. Batch operations [#4-batch-operations]
```ts
// Si actualizas múltiples productos, agrúpalos
export async function updateMultipleProducts(productIds: string[]) {
await db.transaction(async (tx) => {
// Updates en transaction
});
// Revalidar todos juntos
productIds.forEach((id) => {
revalidateTag(`product-${id}`, "max");
});
}
```
**Performance:** Demasiadas revalidaciones simultáneas pueden causar load. Usa
con moderación.
# Caso - Promise compartida (/es/blog/cache-components/shared-promise)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
1. Crear una sola promesa en el padre con `db.getProduct(id)` (sin `await`)
2. Pasar esa promesa a cada componente de campo
3. Envolver cada campo en su propio ``
4. Deconstruir adentro de cada componente: `const { stock } = await productPromise`
Ruta del demo [#ruta-del-demo]
* `/product/[id]/shared-promise`
* Ejemplo directo: `/product/1/shared-promise`
Implementación [#implementación]
```tsx title="app/product/[id]/shared-promise/_components/product-content-shared-promise.tsx"
export async function ProductContentSharedPromise({ params }) {
const { id } = await params;
const safeProductId = parseProductId(id);
// Una sola llamada / una sola promesa
const productPromise = db.getProduct(safeProductId);
return (
<>
}>
}>
}>
>
);
}
```
Cada bloque consume la misma promesa:
```tsx title="Ejemplo de bloque"
export async function ProductStockFromPromise({ productPromise }) {
const { stock, lastChecked } = await productPromise;
return (
{stock} - {lastChecked}
);
}
```
Qué ganas y qué pierdes [#qué-ganas-y-qué-pierdes]
* ✅ Una sola query por request para el payload completo
* ✅ Menos duplicación de lógica de acceso a datos
* ✅ Granularidad visual: cada bloque mantiene su `Suspense`
* ✅ Útil cuando los campos cambian juntos o el registro es pequeño
* ⚠️ Si invalidas ese recurso, se recalcula todo el payload
* ⚠️ Pierdes invalidación fina por campo (texto/precio separado)
* ⚠️ El costo de query puede subir si solo necesitabas un campo
No hay una opción universalmente mejor. Este patrón prioriza simplicidad de
acceso a datos + una sola query, mientras que el enfoque multi-query prioriza
invalidación por campo.
Cuándo usar cada patrón [#cuándo-usar-cada-patrón]
* **Shared promise (`getProduct`)**: cuando quieres una sola query y datos que suelen viajar juntos.
* **Multi-query granular**: cuando texto/precio/stock tienen ciclos de vida distintos y necesitas tags por campo.
Cómo validarlo en la demo [#cómo-validarlo-en-la-demo]
1. Abre `/product/1/shared-promise`
2. Mira la consola del server
3. Deberías ver `getProduct (all fields)` una vez por request en esa ruta
4. Compara con `/product/1`, donde verás queries separadas por campo
# Ivan Bongiovanni - Full Stack Engineer & Next.js Expert (/en)
{/* Live Activity Widgets */}
Career [#career]
Value Proposition [#value-proposition]
Tech Stack [#tech-stack]
In summary [#in-summary]
# Index (/en/blog)
Latest articles [#latest-articles]
Granular Caching Demo: Next.js 16 [#granular-caching-demo-nextjs-16]
Practical tutorial: How to implement `use cache` and `cacheTag` to mix static and dynamic content in the same view.
**Topics:** Next.js 16 · PPR · Granular Caching
***
Scalable Architecture in Next.js 15: The Scope Rule [#scalable-architecture-in-nextjs-15-the-scope-rule]
Discover how to organize your project to scale painlessly. Zero technical debt and instant onboarding.
**Topics:** Architecture · Clean Code · Scalability
***
Rendering Guide in Next.js: SSR, SSG, ISR and PPR [#rendering-guide-in-nextjs-ssr-ssg-isr-and-ppr]
Don't guess which rendering to use. A technical guide to choosing between Server-Side, Static, Incremental, or Partial Prerendering.
**Topics:** Performance · Core Web Vitals · Rendering Patterns
***
About this blog [#about-this-blog]
I write about topics I'm passionate about:
* **Application Architecture** - How to structure scalable projects
* **Web Performance** - Optimization and best practices
* **Next.js and React** - New features and patterns
* **TypeScript** - Strict typing and best practices
* **Applied AI** - Integrating LLMs into products
Is there a topic you'd like me to cover? [Contact me](/).
# Rendering Guide in Next.js: SSR, SSG, ISR, and PPR (/en/blog/nextjs-renders)
Choosing a rendering strategy is not just a technical decision; it defines the **User Experience (UX)**, **SEO**, and **infrastructure costs** of your application.
This technical guide breaks down when and why to use each model in the current Next.js ecosystem.
***
SSR (Server-Side Rendering) [#ssr-server-side-rendering]
*Dynamic Rendering at Request Time*
The HTML is generated on the server **for every request**.
**Ideal for:**
* **Real-time data**: Financial dashboards, social media feeds.
* **Critical personalization**: Pages that depend on cookies or request headers.
* **Dynamic SEO**: Content that changes every second and needs to be indexed.
**Technical Trade-offs:**
* ⚠️ **Higher TTFB (Time to First Byte)**: The user waits for the server to process the page.
* 💸 **Computational Cost**: Requires server execution (Serverless/Node) for every visit.
***
SSG (Static Site Generation) [#ssg-static-site-generation]
*Build Once, Cache Forever*
The HTML is generated during **build time** and distributed globally via CDN.
**Ideal for:**
* **Marketing Pages**: Landings, "About Us", Contact.
* **Documentation**: Content that does not change frequently.
* **Blogs**: Historical articles.
**Advantages:**
* 🚀 **Extreme Performance**: TTFB is insignificant (served from the Edge).
* ✅ **Stability**: If the data API fails, the page continues to work (it's a static file).
***
ISR (Incremental Static Regeneration) [#isr-incremental-static-regeneration]
*Static Content with Dynamic Updates*
Combines the speed of SSG with the freshness of SSR. It allows for regenerating static pages in the background after a time interval (`revalidate`).
**Ideal for:**
* **E-commerce**: Product listings (prices/stock may have a lag of minutes).
* **News Sites**: Content that can tolerate a 60-second delay.
**Mechanism:**
1. User A visits the page → Receives version v1 (fast from cache).
2. Next.js detects that the cache has expired → Starts background regeneration.
3. User B visits the page → Receives updated v2.
***
PPR (Partial Prerendering) [#ppr-partial-prerendering]
*The Future of Hybrid Rendering*
The "silver bullet" of Next.js. It allows the same route to have a **static shell** (SSG) for instant loading, while dynamic parts (like a shopping cart) are loaded via **Streaming**.
**Ideal for:**
* **Modern Applications**: Where the layout is static but the content is dynamic.
* **Core Web Vitals Optimization**: Drastic improvement in LCP (Largest Contentful Paint) and reduction in CLS.
**Benefit:** Eliminates the "data waterfall". The user sees the structure immediately while the data arrives.
***
Decision Matrix [#decision-matrix]
| Strategy | Typical Scenario | Performance (LCP) | Server Cost |
| :------- | :---------------- | :----------------- | :------------ |
| **SSG** | Blog, Landing | ⭐⭐⭐⭐⭐ (Instant) | 📉 Low |
| **ISR** | Product Catalog | ⭐⭐⭐⭐⭐ (Cache Hit) | 📉 Low-Medium |
| **SSR** | Admin Panel, Feed | ⭐⭐ (Depends on DB) | 📈 High |
| **PPR** | Complex App | ⭐⭐⭐⭐ (Hybrid) | ⚖️ Balanced |
***
Conclusion [#conclusion]
There is no universal "best rendering".
* If you can make it static (**SSG**), make it static.
* If you need freshness but can tolerate latency, use **SSR**.
* If you want the best of both worlds for complex apps, go for **PPR**.
As engineers, our job is to align technology with product requirements. Choose the strategy that maximizes value for the end user.
# Scalable Architecture in Next.js 15: The Scope Rule (/en/blog/scope-architecture-nextjs)
Have you ever entered a project and felt lost in a sea of generic folders like `components`, `hooks`, and `utils`?
It's a common symptom of technical debt. As an application scales, the **location** of the code becomes as critical as the quality of the code itself. When everything is "global," nothing has a **clear domain**.
The solution isn't to create more folders. The solution is **intentional architecture** applied through **The Scope Rule**.
The Scope Rule: Context is King [#the-scope-rule-context-is-king]
This architectural principle redefines how we organize code based on its **scope of use**, not on its technical type:
> **"The location of a file is determined by who consumes it."**
Instead of grouping files by their extension or "type" (all buttons together, all hooks together), we group by **business functionality** (Feature Slicing).
1. **Local Scope (1 Feature)**: If a component is exclusive to the Dashboard, it lives **INSIDE** the `dashboard` directory.
2. **Shared Scope (2+ Features)**: Only if a component is used in multiple domains (e.g., Dashboard and Profile) is it promoted to `shared`.
***
Screaming Architecture in the Next.js Era [#screaming-architecture-in-the-nextjs-era]
Your folder structure should "scream" the business intent, not the framework you use. This drastically facilitates the **onboarding** of new developers.
**❌ Silent Structure (Legacy Pattern):**
```text
src/
components/ # Buttons? Modals? Product cards?
hooks/ # Vault logic? Auth?
pages/ # Routes disconnected from their logic
```
**✅ Screaming Structure (Scope Rule):**
```text
src/
app/
(auth)/ # Domain: Authentication
login/
_components/ # Exclusive UI for login (LoginForm)
(dashboard)/ # Domain: Control Panel
analytics/
_components/ # Sales charts (KPIChart)
shared/ # Base UI Kit, reusable primitives
```
**Impact for the team:** A new developer can open the repo and understand **what the application does** in seconds, without navigating complex dependency graphs.
***
Optimization and Server Components [#optimization-and-server-components]
In Next.js 15, this architecture boosts performance and the separation of responsibilities.
* **Server First Mentality**: By keeping components close to their routes (`app/dashboard/_components`), it's natural to write them as Server Components, reducing the JS bundle sent to the client.
* **Actions Placement**: Your `_actions.ts` live next to the form that invokes them. High cohesion, low coupling.
Case Study: Pricing Widget [#case-study-pricing-widget]
Imagine a complex `PriceWidget` that only exists in the Checkout.
* **Traditional Approach**: You put it in `src/components/PriceWidget.tsx`. You contaminate the global scope with specific business logic.
* **Scope Rule Approach**: It lives in `src/app/(shop)/checkout/_components/price-widget.tsx`.
**Result**: Modular code. If tomorrow you remove the checkout feature, you remove its folder and the dead code automatically disappears. **Guaranteed maintainability.**
***
Checklist for a Robust Architecture [#checklist-for-a-robust-architecture]
Before creating a file, apply this decision filter:
1. 🛑 **Scope?**
* 1 Feature → **Local** (`_components`, `_hooks`).
* 2+ Features → **Shared** (`shared/ui`).
2. ⚡ **Environment?**
* Prioritize **Server Components** by default.
* Use `'use client'` only at the leaves of the component tree (specific interactivity).
3. 📢 **Semantics?**
* Use Route Groups `(auth)`, `(shop)` to organize domains without affecting the URL.
***
Architecture isn't rigid rules; it's **communication**. By adopting the Scope Rule, you write code that explains its own purpose, making life easier for your current team and future developers.
# Aliva Shop: Migration & Frontend Architecture (/en/experience/aliva-shop)
Key Tasks [#key-tasks]
* **Code Standardization**: Implementation of BiomeJS to ensure consistency, strict linting, and automatic formatting throughout the repository.
* **Mobile Integration Cycle**: Configuration and generation of production builds (APK/AAB) using Android Studio and Capacitor.
* **Feature Implementation**: Agile development of new business functionalities requested by the client, ensuring scalability.
* **Pixel-Perfect UI**: Faithful translation of Figma designs into reusable and responsive code components.
* **Type Safety**: Introduction of strict typing in TypeScript to reduce runtime errors and improve DX.
* **Technical Documentation**: Creation of exhaustive documentation to facilitate onboarding and future maintenance of the codebase.
Technical Challenge: Legacy Modernization [#technical-challenge-legacy-modernization]
The main objective was to rescue a stagnant codebase and bring it to modern standards to enable new business opportunities.
Migration and Architecture [#migration-and-architecture]
* **Technological Leap**: Execution of the migration from **Angular 15 to 20** and **Ionic 5 to 8**, eliminating blocking technical debt.
* **Reactive Programming**: Adoption of **Signals** for granular state management, replacing `Zone.js` where possible to improve change detection performance.
Critical Features [#critical-features]
* **Precise Geolocation**: Native integration with Capacitor for real-time tracking.
* **Push Notifications**: Push notification system.
Business Impact [#business-impact]
The refactoring wasn't just aesthetic; it unlocked the team's ability to iterate fast. What used to take weeks to implement due to the fragility of legacy code is now deployed in days with confidence.
# Doctor Qali: Frontend Modernization (/en/experience/doctor-qali)
Key Impact [#key-impact]
In an agile HealthTech environment, my role went beyond simple interface implementation to focus on **engineering quality**:
Legacy Code Modernization [#legacy-code-modernization]
* **Strategic Refactoring**: Progressive migration of class components and anti-patterns towards Functional Components and Hooks.
* **Type Safety**: Adoption of **TypeScript** to reduce runtime bugs and improve live code documentation.
Performance & UX [#performance--ux]
* **Component Library**: Implementation of **PrimeReact** to standardize UI, reducing the development time of new features by **40%**.
* **Load Optimization**: Core Web Vitals improvement through code-splitting and critical memoization.
Product Engineering [#product-engineering]
Direct collaboration with product teams to translate complex medical requirements into intuitive and accessible interfaces for doctors and patients.
# Index (/en/experience)
Professional Experience [#professional-experience]
My technical profile focuses on **scalable frontend architecture** and the **modernization of legacy ecosystems**. I transform technical debt into competitive advantages through reactive stacks and robust design patterns.
# Tensolite: Full-Stack Development & Automation (/en/experience/tensolite)
Data Engineering and Backend [#data-engineering-and-backend]
* **Massive Integration**: Development of ETL pipelines for data ingestion and validation from Excel to corporate CRM, reducing human errors by **90%**.
* **Document Automation**: Automatic report and technical PDF generation system, optimizing hundreds of operational hours monthly.
* **Scalable State Management**: Robust architecture with **Redux and Zustand** to handle complex data flows in internal management applications.
* **Admin Panel**: Development of critical modules for the Back-Office, enabling total control over the content and configuration of corporate portals.
Frontend and Corporate UX [#frontend-and-corporate-ux]
* **Pixel-Perfect Design**: Faithful implementation of Figma interfaces into reusable and modular React components.
* **File Management**: Development of secure systems for uploading, viewing, and distributing technical documentation and plans.
Value Delivered [#value-delivered]
Beyond technical execution, I fostered **cross-functional communication** with the development team. I actively participated in defining requirements, pushed technological value propositions, and led joint problem-solving, promoting the adoption of tools that optimized costs and delivery times.
# Better Auth + MercadoPago: Type-Safe Payments (/en/projects/better-auth-mp-plugin)
The Payments Integration Problem [#the-payments-integration-problem]
Integrating payment gateways often introduces **immediate technical debt**:
* **"Black Box" Webhooks**: Untyped payloads that break at runtime.
* **Scattered Logic**: Payment controllers mixed with business logic.
* **Desynchronization**: User state in the DB doesn't match the state in the gateway.
The Solution: Infrastructure as Code [#the-solution-infrastructure-as-code]
This plugin is not just a wrapper; it's a piece of **infrastructure** that standardizes how your application handles money.
Type-Safe Architecture [#type-safe-architecture]
Using **Zod** and **TypeScript**, every interaction with the MercadoPago API is validated. If the payment schema changes, your build fails before it reaches production.
Design Principles [#design-principles]
1. **Strict Decoupling**: Billing logic lives isolated from product logic.
2. **Developer Experience (DX)**: Intelligent autocomplete for plans, subscriptions, and events.
3. **Unified Webhook Handler**: A single endpoint to govern all payment events, with automatic cryptographic signature validation.
Technical Impact [#technical-impact]
* **Boilerplate Reduction**: Eliminates +200 lines of repetitive integration code.
* **Security**: Automatic validation of signatures and payloads.
* **Maintainability**: Centralized updates to the MercadoPago SDK without touching product code.
Designed for teams that value robustness and security in their revenue flows.
# Next.js 16: Cache Components Demo (/en/projects/cache-components-demo)
Why Cache Components? [#why-cache-components]
In modern web development, a page is rarely 100% static or 100% dynamic. The reality is a complex mix:
* **Product Name**: Changes almost never (Static).
* **Price**: Changes occasionally (Revalidatable).
* **Stock**: Changes in real-time (Dynamic).
Until now, managing this required complex segmentation strategies or sacrificing performance. **Next.js 16** introduces primitives to solve this elegantly.
Solution Architecture [#solution-architecture]
This project implements an **Async Decoupled Component** architecture, where each component owns its own data fetching and caching strategy.
```tsx
// ❌ Data Monolith (All or nothing)
const product = await db.getProduct(id);
// ✅ Cache Components (Granularity)
// 'use cache' + cacheLife('weeks')
// 'use cache' + cacheTag('price')
// Dynamic + Streaming
```
Features Implemented [#features-implemented]
1. **Granular Caching**: Independent control per component using the `'use cache'` directive.
2. **Tag-based Revalidation**: Precise invalidation system. A price change doesn't force re-rendering the product description.
3. **Static Shell + Streaming**: The base HTML is served instantly (like SSG), while live data (Stock) comes in via streaming (PPR).
4. **Automatic Optimization**: Next.js automatically parallelizes promises without needing manual `Promise.all`.
Educational Value [#educational-value]
This repository serves as a **testing ground** to understand the future of data-fetching in React. It demonstrates correct `Suspense` patterns, error handling in Server Components, and Zod integration to validate inputs in Server Actions.
It is a living reference for teams planning to migrate to Next.js 16 and adopt PPR (Partial Prerendering).
# Projects and Software Engineering (/en/projects)
Applied Engineering [#applied-engineering]
A collection of developments where I explore advanced architecture patterns, security, and performance at scale.
# Lules Market: Hyperlocal E-commerce (/en/projects/lules-market)
Product Vision: Digitizing the Neighborhood [#product-vision-digitizing-the-neighborhood]
Lules Market was born with a clear mission: **to democratize access to e-commerce** for small merchants who cannot afford custom developments.
It's not just a store; it's a **SaaS ecosystem** where each business manages its own inventory, sales, and subscriptions autonomously.
Architecture for Scaling [#architecture-for-scaling]
The technical challenge was to build a system that feels simple for the user ("Grandma-proof") but is technologically advanced under the hood.
Backend & Data [#backend--data]
* **ElysiaJS (Bun)**: Chosen for its superior performance and low latency in REST API handling.
* **SQL-First with Drizzle**: Efficient, type-safe database queries without the overhead of traditional ORMs. PostgreSQL ensures the referential integrity of thousands of products and transactions.
Frontend & UX [#frontend--ux]
* **Optimistic UI**: Interactions (likes, cart, subscription) feel instantaneous.
* **Accessibility**: `shadcn/ui` components ensure the platform is usable by everyone.
Payment Infrastructure [#payment-infrastructure]
Deep integration with **MercadoPago** to handle the business's financial lifecycle:
1. **Automated Onboarding**: Merchant subscription to monthly plans.
2. **Resilient Webhooks**: Fail-proof system to process payment confirmations even with network intermittency.
3. **Security**: Validation of signatures and tokens to prevent fraud.
Impact [#impact]
Lules Market demonstrates that cutting-edge technology is not just for Silicon Valley startups; it's a powerful tool to **reactive local economies** with global quality software.
# MemesDev: Social Network for Developers (/en/projects/memes-dev)
More Than a Side Project [#more-than-a-side-project]
MemesDev is not just an image gallery; it's a **full-stack social application** designed under production principles.
The goal was to create a **Reference Architecture** for the community, demonstrating how to integrate complex pieces of the modern React ecosystem.
Cutting-Edge Tech Stack [#cutting-edge-tech-stack]
The application orchestrates multiple layers of modern infrastructure:
* **Robust Authentication**: Implementation of **Better Auth** (Magic Links + OAuth) for a frictionless and secure login experience.
* **Data Management**: Modeling complex relationships (Users, Posts, Comments, Likes) with **Prisma ORM** and **PostgreSQL**.
* **End-to-End Validation**: **Zod** ensures data is correct from the client form to the database.
* **Instant UX**: Use of **Optimistic Updates** with TanStack Query/Form so that interaction feels native (zero-latency feel).
Open Source Philosophy [#open-source-philosophy]
The code is public and documented to serve as an educational resource:
1. **Clean Code**: Scalable folder structure.
2. **Strict Linting**: **BiomeJS** configuration to maintain code quality.
3. **Accessibility**: `shadcn/ui` components to ensure inclusion.
Result [#result]
A living platform where humor and high-quality software engineering converge.
# Pikuu: Backend & UI Generator with AI (/en/projects/pikuu)
The Evolution of Code Gen [#the-evolution-of-code-gen]
Pikuu is not another generic code generator that spits out files without context. It is a **technical assistant** designed to integrate into a senior developer's workflow.
Unlike tools that try to "do everything" at once, Pikuu adopts an **iterative and granular** approach:
* Understands the overall project context.
* Modifies only what you ask for.
* Maintains state between iterations.
Engineering Features [#engineering-features]
Pikuu's core is built to respect modern architecture best practices in Next.js:
* **Schema-First Design**: Generates optimized `schema.prisma` from an abstract idea or consumes existing schemas.
* **Type Safety**: Creates **Zod** validation schemas synchronized with your database.
* **Robust Backend**: Writes pure **Server Actions** or Route Handlers, ensuring server and client logic never mix.
* **Production Forms**: Generates complex forms using **shadcn/ui** integrated with **TanStack Form** for state management and validation.
Philosophy: No Placeholders [#philosophy-no-placeholders]
Pikuu's golden rule is: **"If it doesn't run in production, it's useless"**.
1. **Controlled Iteration**: No regenerating the whole project for a small change.
2. **Streaming UI**: Visualize changes in real-time through on-the-fly generated UI artifacts (Generative UI).
3. **Strict TypeScript**: All generated code passes the strictest TypeScript checks.
Tech Stack [#tech-stack]
The tool uses the latest from the Vercel and React ecosystem:
* **AI Engine**: Vercel AI SDK v6 + AI Elements for component streaming.
* **Database**: PostgreSQL + Prisma ORM.
* **Interface**: Framer Motion for smooth transitions and shadcn/ui.
License [#license]
This is a **Source-Available** project. The source code is available for educational and audit purposes, but its commercial use, redistribution, or modification for derivative services is restricted without explicit permission.
Interested in implementing Pikuu in your team? [Let's talk](/).
# Benefits and Costs (/en/blog/cache-components/benefits)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Cache Components don't just improve UX; they can also lower operating costs by reducing work during request time.
What is Really Saved [#what-is-really-saved]
In a granular architecture, the main savings usually come from:
* Fewer queries at request time
* Less render CPU per request
* Lower pressure on traffic spikes
* Better cache hit ratio on stable content
The goal is not "zero queries", but rather to move repetitive request-time
work into cached content controlled by `cacheLife` + tags.
Simple Mental Model [#simple-mental-model]
Think of two layers:
1. **Build/revalidation work**: Amortized cost
2. **Per-request work**: Variable cost (scales with traffic)
When traffic grows, variable cost dominates. That's why reducing per-request work usually has a greater impact on final cost.
Quick Comparison [#quick-comparison]
```tsx
async function ProductPage({ id }) {
const product = await db.query('SELECT * FROM products WHERE id = ?', [id])
return
}
```
* 1 large query per request
* Full render per request
* Lower initial complexity
```tsx
export default function ProductPage({ params }) {
return (
}>
)
}
async function ProductText({ id }) {
'use cache'
cacheTag(`product-text-${id}`)
cacheLife('weeks')
return db.getProductText(id)
}
async function ProductPrice({ id }) {
'use cache'
cacheTag(`product-price-${id}`)
cacheLife('hours')
return db.getProductPrice(id)
}
async function ProductStock({ id }) {
return db.getProductStock(id)
}
```
* Cached text and price
* Only stock remains fresh
* More control over what invalidates and when
Useful Formulas for Estimating Savings [#useful-formulas-for-estimating-savings]
If you define:
* `R` = requests per day
* `Q_all` = queries/request in the traditional approach
* `Q_rt` = queries/request that remain uncached (request time)
Then:
* **Traditional request-time queries** = `R * Q_all`
* **Granular request-time queries** = `R * Q_rt`
* **Request-time savings** = `R * (Q_all - Q_rt)`
Numerical Example [#numerical-example]
* `R = 1,000,000`
* Traditional: `Q_all = 1` (complete query)
* Granular: `Q_rt = 1` (stock only)
In this example, you don't reduce the number of queries/request, but you do:
* Reduce the size and cost of the request-time query
* Serve part of the HTML already resolved
* Improve perceived TTFB through static shell + streaming
If in your traditional case you had several request-time queries and in the granular case you only leave one small one, the savings grow significantly.
Business Benefit (Educational) [#business-benefit-educational]
To explain savings in a demo, speak in terms of:
1. **Variable cost per request** (DB + CPU)
2. **Amortized cost per revalidation**
3. **Perceived latency** (user sees useful content sooner)
For products with high traffic and mostly stable content, the granular pattern
usually reduces variable cost and improves experience at the same time.
Where It Can Be More Expensive [#where-it-can-be-more-expensive]
Not everything should be cached granularly. It can worsen if:
* You fragment into too many components unnecessarily
* You design ambiguous tags or tags that are difficult to invalidate
* You revalidate excessively for very frequent events
Rule of Thumb [#rule-of-thumb]
* Cache granularly only fields with different volatility
* Use the longest acceptable `cacheLife` profile
* Prefer invalidation by specific tags
Implementation Checklist [#implementation-checklist]
* [ ] `cacheComponents: true` enabled
* [ ] Components cached with `'use cache'`
* [ ] `cacheTag` per functional unit (e.g., `product-price-${id}`)
* [ ] `revalidateTag` / `updateTag` based on required consistency
* [ ] Runtime data inside `Suspense`
* [ ] Measurement with logs and headers (`x-nextjs-cache`)
What to Measure in Production [#what-to-measure-in-production]
Minimum recommended metrics:
* P95/P99 latency on critical pages
* HIT/MISS/STALE ratio per route
* Queries per request (average and percentiles)
* CPU per request in SSR
With these 4 metrics, you can justify cost savings with data, not just theory.
# Key Concepts (/en/blog/cache-components/concepts)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
1. Async Component = Promise [#1-async-component--promise]
The most important concept to understand:
```tsx
// This async component RETURNS a promise
async function ProductStock({ productId }) {
const stock = await db.getStock(productId);
return
{stock}
;
}
// That's why the Suspense goes in the PARENT:
function Parent() {
return (
{/* ← This line CREATES and EXECUTES the promise */}
);
}
```
When React renders ``, it is invoking an async function, which creates a promise. The Suspense in the parent **captures** that promise.
**Common Error:** Trying to put Suspense INSIDE the async component.
❌ Incorrect [#-incorrect]
```tsx
async function ProductStock() {
return (
{/* The await has already happened, this won't work */}
{await db.query(...)}
)
}
```
**Why it doesn't work:**
* The `await` executes BEFORE the `return`.
* By the time you reach the `return`, the promise has already been resolved.
* Suspense has nothing to capture.
✅ Correct [#-correct]
```tsx
function Parent() {
return (
{/* Suspense captures the execution of the component */}
)
}
async function ProductStock() {
const stock = await db.query(...)
return
{stock}
}
```
2. Multiple Queries vs. Cache [#2-multiple-queries-vs-cache]
The Common Question [#the-common-question]
> "Isn't it inefficient to make 3 separate queries for the same product?"
**Answer:** It depends on when they are executed.
Traditional Model [#traditional-model]
```tsx
// One query in each request
async function ProductPage({ productId }) {
const product = await db.query('SELECT * FROM products')
return (
{product.name}
${product.price}
Stock: {product.stock}
)
}
```
**Timeline:**
* Request 1: Complete query
* Request 2: Complete query
* Request 3: Complete query
**Total:** 3 queries to the DB
Granular Model [#granular-model]
```tsx
// 3 queries, but only 1 at request time
function ProductPage({ productId }) {
return (
<>
{/* Build time */}
{/* Build time */}
{/* Request time */}
>
)
}
```
**Timeline:**
* Build: 2 queries (text + price)
* Request 1: 1 query (stock)
* Request 2: 1 query (stock)
* Request 3: 1 query (stock)
**Total:** 2 queries at build + 3 queries at request = 5 queries
But each request only executes 1 small query, and the HTML comes pre-generated.
Conscious Trade-off [#conscious-trade-off]
| Aspect | Single Query | Separate Queries |
| --------------------- | ---------------------- | ----------------------------- |
| Total Queries | 3 (1 per request) | 5 (2 at build + 3 at request) |
| Queries per Request | 1 complete query | 1 small query (stock) |
| Granular Control | ❌ All or nothing | ✅ Per field |
| Perceived Performance | Depends on total cache | Instant HTML + streaming |
| Complexity | Low | Medium |
**Conclusion:** More total queries, but better user experience and fine-grained control.
3. Tags for Revalidation [#3-tags-for-revalidation]
Tags allow for **selective** and **precise** cache invalidation.
Without Tags (Traditional) [#without-tags-traditional]
```tsx
// Invalidate the entire page
revalidatePath("/product/1");
// Invalidates THE WHOLE product:
// - Text (rarely changes)
// - Price (the only one that changed)
// - Stock (which already has no cache)
```
With Tags (Granular) [#with-tags-granular]
```tsx
// Cache with unique tags
async function ProductPrice({ productId }) {
"use cache";
cacheTag(`product-price-${productId}`);
// ...
}
// Invalidate only the price
revalidateTag(`product-price-${productId}`, "max");
// Text keeps its cache ✅
// Stock already has no cache ✅
```
Anatomy of a Tag [#anatomy-of-a-tag]
```tsx
cacheTag(`product-price-${productId}`);
// Final Tag: "product-price-1"
// Allows: revalidateTag('product-price-1', 'max')
```
**Advantage:** Surgical revalidation without affecting other fields.
4. Runtime Data Requires Suspense [#4-runtime-data-requires-suspense]
Runtime data is information that only exists when a request arrives:
* `params` - URL parameters
* `searchParams` - Query strings
* `cookies()` - User cookies
* `headers()` - Request headers
Why They Need Suspense [#why-they-need-suspense]
```tsx
// ❌ This triggers an error
export default async function ProductPage({ params }) {
const { id } = await params; // Runtime data without Suspense
return
{id}
;
}
// Error: Runtime data was accessed outside of
```
**Reason:** During prerendering (build time), `params` does not exist. Next.js requires you to explicitly mark that this content needs a request context.
Solution [#solution]
```tsx
// ✅ Correct
export default function ProductPage({ params }) {
return (
}>
{/* params passed as a prop */}
)
}
async function ProductContent({ params }) {
const { id } = await params {/* Now it IS inside Suspense */}
return
{id}
}
```
The pattern is: a sync component passes runtime data as a prop to an async
component inside Suspense.
5. Static Shell vs. Streaming [#5-static-shell-vs-streaming]
Static Shell [#static-shell]
Content that is prerendered and included in the initial HTML:
```tsx
// These components with 'use cache' generate static shell
// → HTML included
// → HTML included
```
**Advantage:** The user sees content instantly, without waiting for queries.
Streaming [#streaming]
Content that is rendered at request time and streamed to the browser:
```tsx
// This component without 'use cache' streams
}>
// → Streams in later
```
**Advantage:** Content is always fresh without blocking the initial HTML.
Visualization [#visualization]
```
User Request → Server
↓
[Prerendering check]
↓
┌──────────┴──────────┐
↓ ↓
Static Shell Streaming
(instant) (on-demand)
↓ ↓
Browser ←────────────────┘
↓
[Shows static + skeleton]
↓
[Streaming replaces skeleton]
↓
[Complete page rendered]
```
6. cacheLife Profiles [#6-cachelife-profiles]
Predefined profiles for different types of content:
```tsx
cacheLife("default"); // General balance
cacheLife("minutes"); // Volatile (e.g., trending topics)
cacheLife("hours"); // Changes occasionally (e.g., prices)
cacheLife("days"); // Changes rarely (e.g., categories)
cacheLife("weeks"); // Very stable (e.g., legal terms)
cacheLife("max"); // Almost never changes (e.g., historical content)
```
Or define your own profile:
```tsx
cacheLife({
stale: 3600, // 1 hour until considered stale
revalidate: 7200, // 2 hours until revalidation
expire: 86400, // 1 day until complete expiration
});
```
**Rule of thumb:** Use the longest profile acceptable for your use case. More
cache = better performance.
# Implementation (/en/blog/cache-components/implementation)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
import { Step, Steps } from "fumadocs-ui/components/steps";
Enable Cache Components [#enable-cache-components]
Update `next.config.mjs`:
```js title="next.config.mjs"
const config = {
cacheComponents: true, // ← Enable Cache Components
};
export default config;
```
This flag enables **Partial Prerendering (PPR)** automatically.
Generate static routes for the dynamic segment [#generate-static-routes-for-the-dynamic-segment]
For Next.js to know which products to prerender for `/product/[id]`, add `generateStaticParams`:
```tsx title="app/product/[id]/page.tsx"
import { db } from "@/lib/db";
export async function generateStaticParams() {
const products = await db.listProducts();
return products.map((product) => ({
id: product.id,
}));
}
```
Without `generateStaticParams`, the dynamic route still works, but you lose
part of the educational value of clearly seeing which paths are prerendered.
Create separate queries [#create-separate-queries]
Each field needs its own query:
```ts title="lib/db.ts"
export const db = {
// Query ONLY for text
async getProductText(productId: string) {
console.log(`[DB Query] 📝 getProductText - Product ${productId}`);
const { name, description } = await db.query(
"SELECT name, description FROM products WHERE id = ?",
[productId]
);
return { name, description };
},
// Query ONLY for price
async getProductPrice(productId: string) {
console.log(`[DB Query] 💰 getProductPrice - Product ${productId}`);
const { price } = await db.query(
"SELECT price FROM products WHERE id = ?",
[productId]
);
return { price };
},
// Query ONLY for stock
async getProductStock(productId: string) {
console.log(`[DB Query] 📦 getProductStock - Product ${productId}`);
const { stock } = await db.query(
"SELECT stock FROM products WHERE id = ?",
[productId]
);
return { stock, lastChecked: new Date().toISOString() };
},
};
```
**Why multiple queries?** Although it seems inefficient, cached queries are
executed at **build time**, not on every request.
Text component (cached) [#text-component-cached]
```tsx title="app/product/[id]/_components/product-text.tsx"
async function ProductText({ productId }: { productId: string }) {
"use cache";
cacheTag(`product-text-${productId}`);
cacheLife("weeks"); // Long cache: rarely changes
const { name, description } = await db.getProductText(productId);
return (
{name}
📦 Cached 1 week
{description}
);
}
```
**Key points:**
* `'use cache'` marks the component as cacheable
* `cacheTag` allows selective revalidation
* `cacheLife('weeks')` defines cache duration
* Specific query just for this field
Price component (cached) [#price-component-cached]
```tsx title="app/product/[id]/_components/product-price.tsx"
async function ProductPrice({ productId }: { productId: string }) {
"use cache";
cacheTag(`product-price-${productId}`);
cacheLife("hours"); // Medium cache: changes occasionally
const { price } = await db.getProductPrice(productId);
return (
{/* Text and price: cached, go to static shell */}
{/* Stock: no cache, requires additional Suspense */}
}>
);
}
```
**Critical:** `params` is runtime data and must be accessed inside Suspense.
Server Actions for revalidation [#server-actions-for-revalidation]
```ts title="app/actions.ts"
"use server";
import { revalidateTag } from "next/cache";
import { z } from "zod";
const productIdSchema = z
.string()
.trim()
.min(1)
.regex(/^[a-zA-Z0-9-]+$/);
function validateProductId(productId: string) {
return productIdSchema.parse(productId);
}
export async function revalidateProductPrice(productId: string) {
const safeProductId = validateProductId(productId);
console.log(`[Server Action] Revalidating price for ${safeProductId}`);
revalidateTag(`product-price-${safeProductId}`, "max");
return { success: true, message: "Price revalidated" };
}
export async function revalidateProductText(productId: string) {
const safeProductId = validateProductId(productId);
console.log(`[Server Action] Revalidating text for ${safeProductId}`);
revalidateTag(`product-text-${safeProductId}`, "max");
return { success: true, message: "Text revalidated" };
}
```
Execution Timeline [#execution-timeline]
```
pnpm build
✅ generateStaticParams lists static product IDs
✅ ProductText executes query → result in static shell
✅ ProductPrice executes query → result in static shell
❌ ProductStock DOES NOT execute (requires request)
Output:
- HTML with text and price already rendered
- Placeholder for stock (Suspense fallback)
```
```
User visits /product/1
⚡ Browser receives HTML immediately
- Text: "Laptop Dell XPS 15" (from static shell)
- Price: "$1,299.99" (from static shell)
- Stock: (placeholder)
🔄 Server executes ProductStock
- Query DB for current stock
⚡ Stock streams to the browser
- Replaces
- User sees: "15 units"
```
```
User clicks "Revalidate Price"
🔄 revalidateProductPrice() executes
- Marks 'product-price-1' as stale
📝 Next visit to /product/1:
- Text: Still from cache (not revalidated)
- Price: Regenerated in background
- Stock: Runs fresh (no cache)
```
Verification [#verification]
In development [#in-development]
```bash
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) and check the server console:
```
[DB Query] 📝 getProductText - Product 1
[DB Query] 💰 getProductPrice - Product 1
[DB Query] 📦 getProductStock - Product 1
```
In production [#in-production]
```bash
pnpm build
pnpm start
```
Check the build output:
```
Route (app) Size First Load JS
| ◐ / 2.4 kB 170 kB
| ◐ /product/[id]
◐ Partial Prerender - static HTML + dynamic streaming content
```
If you see `◐ Partial Prerender` for `/product/[id]`, it's working correctly
for this Cache Components demo.
Debugging [#debugging]
Seeing what is cached [#seeing-what-is-cached]
In the browser, View Source (Ctrl+U) and look for:
```html
Laptop Dell XPS 15
Powerful laptop with processor...
$1,299.99
```
Useful logs [#useful-logs]
Add logs in each component:
```tsx
async function ProductPrice({ productId }) {
"use cache";
console.log(`[${new Date().toISOString()}] Rendering ProductPrice`);
// ...
}
```
In build, you should see the log only once.
In request time, you SHOULD NOT see it (it uses cache).
# Cache Components (/en/blog/cache-components)
import { Callout } from "fumadocs-ui/components/callout";
import { Card, Cards } from "fumadocs-ui/components/card";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Interactive demo of how to cache **individual fields** of a record with
different strategies in Next.js 16, improving perceived latency and reducing
infrastructure load during request time.
The Problem [#the-problem]
Imagine you have a **product** with 3 fields:
* **Text** (name + description): Rarely changes
* **Price**: Changes occasionally
* **Stock**: Must always be updated
How do you cache each field independently?
❌ What DOES NOT work [#-what-does-not-work]
```tsx
// Trying to selectively cache from a single object
async function ProductPage({ productId }) {
// A single query brings everything
const product = await db.query("SELECT * FROM products WHERE id = ?");
// How do I cache only the price?
// How do I NOT cache the stock?
return (
);
}
```
**It doesn't work because:** Once you have the complete object, you cannot selectively apply `use cache` to individual fields.
✅ The Solution [#-the-solution]
Each field is a **separate async component** with its own query and cache strategy.
```tsx
export default function ProductPage({ params }) {
return (
}>
);
}
async function ProductContent({ params }) {
const { id } = await params;
return (
{/* FIELD 1: Cached text */}
{/* FIELD 2: Cached price */}
{/* FIELD 3: Stock without cache */}
}>
);
}
```
**Important:** The Suspense goes in the **parent**, not inside the async
component.
Architecture [#architecture]
```
ProductPage (sync - static)
│
└─
└─ ProductContent (async - accesses params)
├─ ProductText (async + 'use cache')
├─ ProductPrice (async + 'use cache')
└─
└─ ProductStock (async without cache)
```
Visual Comparison [#visual-comparison]
At build time, Next.js prerenders the page: - ✅ `ProductText` executes
query → included in static shell - ✅ `ProductPrice` executes query →
included in static shell - ❌ `ProductStock` DOES NOT execute (requires
request context) **Result:** HTML with text and price already rendered
When a user visits the page: 1. ⚡ Browser receives HTML instantly (text +
price) 2. ⚡ Browser shows stock skeleton 3. 🔄 Server executes stock query
4\. ⚡ Stock streams in and replaces skeleton
Next steps [#next-steps]
# Revalidation (/en/blog/cache-components/revalidation)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Next.js 16 offers two ways to invalidate cache: `revalidateTag` and `updateTag`. Both work with tags but have different behaviors.
updateTag vs. revalidateTag [#updatetag-vs-revalidatetag]
Stale-while-revalidate [#stale-while-revalidate]
```ts title="app/actions.ts"
'use server'
import { revalidateTag } from 'next/cache'
export async function revalidatePrice(productId: string) {
revalidateTag(`product-price-${productId}`, 'max')
// ↑
// Revalidation Profile
}
```
Behavior [#behavior]
1. **Marks the cache as "stale"**
2. **Continues serving old content** while regenerating in the background
3. **The next request** gets the new content
Timeline [#timeline]
```
t=0s User 1 clicks "Revalidate"
→ Cache marked as stale
→ Continues serving old version
t=1s User 2 visits the page
→ Receives old version
→ Triggers background regeneration
t=3s Regeneration complete
t=5s User 3 visits the page
→ Receives NEW version ✨
```
Available Profiles [#available-profiles]
```ts
revalidateTag('my-tag', 'max') // Most aggressive
revalidateTag('my-tag', 'default') // Balanced
revalidateTag('my-tag', 'min') // Most conservative
// Or custom
revalidateTag('my-tag', {
stale: 3600,
revalidate: 7200,
expire: 86400
})
```
Use when [#use-when]
✅ Immediate change is not critical\
✅ You can tolerate old data briefly\
✅ Blog posts, prices, editorial content\
✅ You want to minimize latency for users
Real Example [#real-example]
```ts title="app/actions.ts"
'use server'
import { revalidateTag } from 'next/cache'
// Update product price
export async function updateProductPrice(
productId: string,
newPrice: number
) {
// 1. Update in DB
await db.query(
'UPDATE products SET price = ? WHERE id = ?',
[newPrice, productId]
)
// 2. Revalidate cache (stale-while-revalidate)
revalidateTag(`product-price-${productId}`, 'max')
return { success: true }
}
```
Immediate Invalidation [#immediate-invalidation]
```ts title="app/actions.ts"
'use server'
import { updateTag } from 'next/cache'
export async function updateCartItem(userId: string) {
updateTag(`user-cart-${userId}`)
// Second argument not required
}
```
Behavior [#behavior-1]
1. **Expires the cache immediately**
2. **The next request regenerates** the content
3. **User gets fresh content** in the same request
Timeline [#timeline-1]
```
t=0s User 1 clicks "Add to cart"
→ Cache EXPIRES immediately
t=1s User 2 visits the page
→ Cache expired, regenerates on request
→ Receives NEW version ✨
t=2s User 3 visits the page
→ Receives NEW version ✨
```
Use when [#use-when-1]
✅ Change must be immediate\
✅ You cannot tolerate old data\
✅ Shopping cart\
✅ Likes, votes, reactions\
✅ User mutations
Real Example [#real-example-1]
```ts title="app/actions.ts"
'use server'
import { updateTag } from 'next/cache'
// Add item to cart
export async function addToCart(
userId: string,
productId: string
) {
// 1. Add to DB
await db.query(
'INSERT INTO cart_items (user_id, product_id) VALUES (?, ?)',
[userId, productId]
)
// 2. Invalidate cache immediately
updateTag(`user-cart-${userId}`)
return { success: true }
}
```
Direct Comparison [#direct-comparison]
| Aspect | revalidateTag | updateTag |
| ------------------- | ---------------------- | ------------------------------- |
| **Behavior** | Stale-while-revalidate | Immediate Invalidation |
| **Latency** | Low (serves old first) | Medium (regenerates on request) |
| **Consistency** | Eventual | Immediate |
| **Performance** | Better | Good |
| **Use case** | Editorial content | User mutations |
| **Second argument** | Yes (profile) | No |
Use Case Examples [#use-case-examples]
Blog / Editorial Content [#blog--editorial-content]
```ts title="app/actions.ts"
"use server";
import { revalidateTag } from "next/cache";
export async function publishBlogPost(postId: string) {
await db.query("UPDATE posts SET published = true WHERE id = ?", [postId]);
// Stale-while-revalidate: OK that some see old version
revalidateTag(`blog-post-${postId}`, "max");
revalidateTag("blog-list", "max");
}
```
E-commerce / Cart [#e-commerce--cart]
```ts title="app/actions.ts"
"use server";
import { updateTag } from "next/cache";
export async function addToCart(userId: string, productId: string) {
await db.query("INSERT INTO cart_items (user_id, product_id) VALUES (?, ?)", [
userId,
productId,
]);
// Immediate invalidation: user must see their cart updated NOW
updateTag(`user-cart-${userId}`);
}
```
Likes System [#likes-system]
```ts title="app/actions.ts"
"use server";
import { updateTag } from "next/cache";
export async function likePost(userId: string, postId: string) {
await db.query("INSERT INTO likes (user_id, post_id) VALUES (?, ?)", [
userId,
postId,
]);
// Immediate invalidation: user must see their like reflected
updateTag(`post-likes-${postId}`);
updateTag(`user-liked-posts-${userId}`);
}
```
Product Prices [#product-prices]
```ts title="app/actions.ts"
"use server";
import { revalidateTag } from "next/cache";
export async function updatePrice(productId: string, newPrice: number) {
await db.query("UPDATE products SET price = ? WHERE id = ?", [
newPrice,
productId,
]);
// Stale-while-revalidate: OK that the price takes a bit to update
revalidateTag(`product-price-${productId}`, "max");
}
```
Revalidating Multiple Tags [#revalidating-multiple-tags]
Both functions can be called multiple times:
```ts title="app/actions.ts"
"use server";
import { revalidateTag, updateTag } from "next/cache";
export async function updateProduct(productId: string, data: ProductData) {
await db.query("UPDATE products SET ... WHERE id = ?", [productId]);
// Mix both approaches as needed
revalidateTag(`product-text-${productId}`, "max"); // Text: stale-while-revalidate
revalidateTag(`product-price-${productId}`, "max"); // Price: stale-while-revalidate
updateTag(`product-inventory-${productId}`); // Inventory: immediate
}
```
revalidatePath vs. Tags [#revalidatepath-vs-tags]
revalidatePath (traditional) [#revalidatepath-traditional]
```ts
revalidatePath("/product/1");
```
**Invalidates:** The whole route `/product/1`
**Problem:** You cannot invalidate specific fields
Tags (granular) [#tags-granular]
```ts
revalidateTag("product-price-1", "max");
```
**Invalidates:** Only the price cache of product 1
**Advantage:** Surgical, does not affect other fields
**General rule:** Use tags for granular control. Only use `revalidatePath`
when you want to invalidate an entire page.
Debugging Revalidation [#debugging-revalidation]
Useful Logs [#useful-logs]
```ts title="app/actions.ts"
"use server";
import { revalidateTag } from "next/cache";
export async function revalidatePrice(productId: string) {
console.log(
`[Revalidation] product-price-${productId} at ${new Date().toISOString()}`
);
revalidateTag(`product-price-${productId}`, "max");
}
```
Verify in Browser [#verify-in-browser]
1. Click "Revalidate"
2. Refresh the page
3. View Source (Ctrl+U)
4. Look for updated content in the HTML
If the content changed, revalidation worked.
Response Headers [#response-headers]
Next.js includes useful headers:
```
x-nextjs-cache: HIT | MISS | STALE
```
* `HIT` - Content from cache
* `MISS` - Regenerated
* `STALE` - Stale-while-revalidate in progress
Best Practices [#best-practices]
1. Use the Correct Approach [#1-use-the-correct-approach]
```ts
// ✅ Editorial content
revalidateTag("blog-post", "max");
// ✅ User mutations
updateTag("user-cart");
```
2. Descriptive Tags [#2-descriptive-tags]
```ts
// ✅ Good
cacheTag(`product-price-${productId}`);
cacheTag(`user-profile-${userId}`);
// ❌ Bad
cacheTag("data", id);
cacheTag("cache1", value);
```
3. Revalidate Related Tags [#3-revalidate-related-tags]
```ts
// Update product → revalidate multiple tags
revalidateTag(`product-${productId}`, "max");
revalidateTag("product-list", "max");
revalidateTag(`category-${categoryId}`, "max");
```
4. Batch Operations [#4-batch-operations]
```ts
// If you update multiple products, group them
export async function updateMultipleProducts(productIds: string[]) {
await db.transaction(async (tx) => {
// Updates in transaction
});
// Revalidate all together
productIds.forEach((id) => {
revalidateTag(`product-${id}`, "max");
});
}
```
**Performance:** Too many simultaneous revalidations can cause load. Use
sparingly.
# Case - Shared Promise (/en/blog/cache-components/shared-promise)
import { Callout } from "fumadocs-ui/components/callout";
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
1. Create a single promise in the parent with `db.getProduct(id)` (without `await`)
2. Pass that promise to each field component
3. Wrap each field in its own ``
4. Deconstruct inside each component: `const { stock } = await productPromise`
Demo Route [#demo-route]
* `/product/[id]/shared-promise`
* Direct example: `/product/1/shared-promise`
Implementation [#implementation]
```tsx title="app/product/[id]/shared-promise/_components/product-content-shared-promise.tsx"
export async function ProductContentSharedPromise({ params }) {
const { id } = await params;
const safeProductId = parseProductId(id);
// A single call / a single promise
const productPromise = db.getProduct(safeProductId);
return (
<>
}>
}>
}>
>
);
}
```
Each block consumes the same promise:
```tsx title="Block Example"
export async function ProductStockFromPromise({ productPromise }) {
const { stock, lastChecked } = await productPromise;
return (
{stock} - {lastChecked}
);
}
```
What You Gain and What You Lose [#what-you-gain-and-what-you-lose]
* ✅ A single query per request for the complete payload
* ✅ Less duplication of data access logic
* ✅ Visual granularity: each block maintains its own `Suspense`
* ✅ Useful when fields change together or the record is small
* ⚠️ If you invalidate that resource, the entire payload is recalculated
* ⚠️ You lose fine-grained invalidation per field (separate text/price)
* ⚠️ Query cost **may** increase if you only needed one field
There is no universally better option. This pattern prioritizes data access
simplicity + a single query, while the multi-query approach prioritizes
field-level invalidation.
When to Use Each Pattern [#when-to-use-each-pattern]
* **Shared promise (`getProduct`)**: When you want a single query and data that usually travels together.
* **Granular multi-query**: When text/price/stock have different lifecycles and you need tags per field.
How to Validate It in the Demo [#how-to-validate-it-in-the-demo]
1. Open `/product/1/shared-promise`
2. Look at the server console
3. You should see `getProduct (all fields)` once per request on that route
4. Compare with `/product/1`, where you will see separate queries per field