Next.js 16, Componentes de caché y React Compiler: lo que realmente cambia
· 7 min read · Filippo Spinella · Next.js, React, Frontend, Performance
Durante años una de las preguntas más molestas en Next.js ha sido: "¿Esta página es estática o dinámica?". Parece una pregunta simple, hasta que agregas una llamada a cookies(), un fetch con diferentes opciones, un cliente de base de datos, un CMS, un carrito de compras o un contenido personalizado.
Next.js 16 es interesante porque intenta hacer que esta conversación sea menos misteriosa. No elimina la complejidad, pero cambia el modelo mental: las rutas son dinámicas de forma predeterminada, el caché se declara cuando es necesario y Suspense se convierte en la forma natural de componer shells rápidos con partes que se mantienen actualizadas.
La característica que hay que entender son los componentes de caché. Stable Turbopack, React Compiler, proxy.ts y las nuevas API de invalidación son importantes, pero giran en torno al mismo problema: crear aplicaciones rápidas sin tener que adivinar qué decidió el marco detrás de escena.
Porque esto importa
En una aplicación real no sólo hay "páginas estáticas" y "páginas dinámicas". Tienes diferentes piezas con diferentes necesidades.
La ficha del producto puede cambiar algunas veces al día. El precio puede cambiar más a menudo. La disponibilidad debe ser casi activa. El nombre de usuario es personal. Las reseñas se pueden transmitir. La barra lateral puede ser estable. El carro no.
Si tratas todo como una unidad, siempre terminas en uno de dos extremos:
- almacenamiento en caché agresivo y riesgo de ver datos antiguos;
- renderizado dinámico en todas partes y rendimiento peor de lo necesario.
Cache Components sirve precisamente para evitar esta falsa elección.
El modelo en la práctica.
Con cacheComponents: true, puedes declarar qué se puede almacenar en caché usando "use cache". Luego puede asociar la duración y las etiquetas con cacheLife() y cacheTag(). Las partes dinámicas siguen siendo dinámicas y se pueden aislar con Suspense.
La configuración es pequeña:
// next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { cacheComponents: true, }; export default nextConfig;
El gran cambio no está en la configuración. Está en cómo empiezas a escribir los componentes.
// app/products/[slug]/page.tsx import { Suspense } from 'react'; import { cacheLife, cacheTag } from 'next/cache'; async function getProduct(slug: string) { 'use cache'; cacheLife('hours'); cacheTag(`product:${slug}`); return db.product.findUnique({ where: { slug } }); } async function ProductDetails({ slug }: { slug: string }) { const product = await getProduct(slug); return ( <section> <h1>{product.name}</h1> <p>{product.description}</p> </section> ); } async function LiveInventory({ slug }: { slug: string }) { const inventory = await db.inventory.findFirst({ where: { slug } }); return <p>{inventory.quantity} pezzi disponibili</p>; } export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; return ( <> <ProductDetails slug={slug} /> <Suspense fallback={<p>Controllo disponibilità...</p>}> <LiveInventory slug={slug} /> </Suspense> </> ); }
No es necesario que la página esté toda en caché o dinámica. La ficha de producto puede ser rápida y reutilizable. El inventario puede mantenerse actualizado. El usuario ve algo de inmediato, sin esperar la parte más lenta.
use cache es documentación ejecutable
Lo que me gusta de "use cache" es que te obliga a hacer explícita una intención. Cuando lees una función, comprendes inmediatamente que alguien ha decidido: "estos datos se pueden reutilizar".
Es especialmente útil cuando no estás usando fetch. Muchas aplicaciones leen datos de Prisma, Drizzle, SDK internos, clientes CMS o funciones de servicio. En esos casos, el antiguo razonamiento basado únicamente en las opciones fetch no era suficiente.
Una regla general:
- contenido de caché relativamente estable;
- utilizar etiquetas granulares;
- deja permisos dinámicos, sesiones, carritos, notificaciones y estados transaccionales;
- poner partes lentas dentro de
Suspense; - medir antes de decir "mejoramos el rendimiento".
Invalidar sin tirarlo todo
El caché sólo es útil si puedes actualizarlo con precisión. Aquí cacheTag, revalidateTag y updateTag se vuelven importantes.
Ejemplo:
'use server'; import { updateTag } from 'next/cache'; export async function updateProductName(productId: string, name: string) { await db.product.update({ where: { id: productId }, data: { name }, }); updateTag(`product:${productId}`); }
El detalle importante es la etiqueta. product:${productId} indica un límite preciso. products dice un cubo enorme. Al principio el enorme cubo resulta cómodo; después de unos meses se convierte en el motivo por el que invalidas media aplicación para cambiar un título.
Turbopack estable: la parte que escuchas todos los días
Next.js 16 lleva Turbopack al centro de desarrollo y construcción. No es la característica más poética, pero es la que sientes mientras trabajas: servidor que se inicia antes, actualización más rápida, compilaciones que dejan de parecer una pausa para el café forzada.
Dicho esto, no migraría una base de código llena de complementos personalizados con los ojos cerrados. Yo comprobaría:
- construcción local;
- importación no estándar;
- MDX, SVG y CSS;
- Quedan complementos de paquete web;
- páginas críticas;
- diferencias en los tiempos de construcción.
Para proyectos nuevos, comenzaría desde el valor predeterminado. Para los maduros, haría una migración medida.
React Compiler: elimina el ruido, no el pensamiento
React Compiler 1.0 es estable y Next.js 16 lo admite con reactCompiler. La promesa es reducir mucha memorización manual: menos memo, menos useMemo, menos useCallback utilizados "por seguridad".
// next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { reactCompiler: true, }; export default nextConfig;
No lo trataría como un polvo mágico. El compilador ayuda cuando el código sigue bien las reglas de React. Si los componentes tienen efectos secundarios extraños, mutaciones ocultas o ganchos mal utilizados, eso debe solucionarse primero.
La forma saludable de probarlo:
- actualizar
eslint-plugin-react-hooks; - corregir violaciones reales;
- habilitarlo en un área controlada;
- medir el tiempo de construcción y el comportamiento;
- elimine la memorización manual sólo cuando ya no sea necesaria.
El objetivo no es borrar todos los useMemo. El objetivo es dejar de escribir memorización preventiva porque tenemos miedo de renderizar.
proxy.ts y el límite de la red
El antiguo middleware.ts se convierte en proxy.ts. Es un cambio de nombre, pero tiene sentido: ese archivo se encuentra en el límite de la solicitud, no es un middleware genérico tradicional de estilo backend.
// proxy.ts import { NextRequest, NextResponse } from 'next/server'; export default function proxy(request: NextRequest) { const isLoggedIn = Boolean(request.cookies.get('session')); if (!isLoggedIn && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); }
La regla aquí es simple: mantenlo pequeño. Redirección, enrutamiento de autenticación, encabezados, reescrituras esenciales. Si empieza a parecer un segundo backend, probablemente esté haciendo demasiado.
Cómo realmente migraría
No activaría todas las funciones a la vez. Yo haría esto:
- Actualizo Next, React y React DOM;
- Lanzo el codemod oficial;
- Arreglé cambios importantes en
params,searchParams,cookies(),headers()ydraftMode(); - Migro
middleware.tsaproxy.ts; - Reviso compilaciones y páginas críticas;
- Habilito Componentes de caché en una sección donde el caché actualmente crea fricción;
- Defino convenciones para etiquetas e invalidación;
- Intento React Compiler por separado;
- Comparación de métricas antes y después.
La buena migración no es la que utiliza todas las funciones nuevas. Es lo que hace que el comportamiento de la aplicación sea más legible.
¿Qué cambia en la forma de pensar?
Lo más útil de Next.js 16 es que te obliga a nombrar mejor las intenciones. Una función no es simplemente "obtener el producto de la base de datos". Es "obtener el producto, puedo almacenarlo en caché durante horas, lo invalido con esta etiqueta". Un componente no es simplemente "renderizar la página". Es "este es el caparazón rápido, esta pieza es personal, esto viene en streaming".
Al principio parece más trabajo. Entonces se convierte en una forma de calma. Las decisiones de desempeño ya no están ocultas en una combinación de valores predeterminados, heurísticas y memoria tribal. Están en el código.