Guía de migración a Next.js 16: Turbopack, proxy.ts, Cache Components y todos los breaking changes explicados
Tu pipeline de CI acaba de fallar después de subir next a ^16.0.0. El log de errores tiene 400 líneas, la mitad de tu middleware está roto, y apareció una nueva convención proxy.ts que nunca viste. Bienvenido a la actualización de Next.js 16.
Next.js 16 es el release más significativo en términos de arquitectura desde que el App Router llegó en v13. Webpack murió — Turbopack es el único bundler. La convención middleware.ts fue reemplazada por proxy.ts. Todo el modelo de caché fue reconstruido alrededor de Cache Components y la directiva use cache. Y si estás corriendo en contenedores, hay cambios de comportamiento de memoria que te van a morder en producción si no los conocés.
Esta guía recorre cada breaking change, explica por qué se hizo, te da los comandos exactos de codemod, y el camino de migración manual cuando los codemods no alcanzan. Ya sea que estés actualizando un sitio de marketing chico o una app enterprise de 200 rutas, este documento es todo lo que necesitás.
Qué cambió y por qué
Antes de entrar en los pasos de migración, acá va el panorama general de lo que Next.js 16 cambió y la razón detrás de cada decisión:
┌─────────────────────────────────────────────────────────────┐
│ Arquitectura Next.js 16 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Turbopack │ │ proxy.ts │ │ Cache │ │
│ │ (Único │ │ (Reemplaza │ │ Components │ │
│ │ Bundler) │ │ middleware) │ │ ('use cache') │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬────────┘ │
│ │ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ ┌────────┴────────┐ │
│ │ HMR & Build │ │ Límites de │ │ Caché │ │
│ │ 10x más │ │ red claros │ │ declarativo con │ │
│ │ rápido │ │ │ │ TTL granular │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ APIs de │ │ PPR │ │
│ │ Request │ │ (Default) │ │
│ │ Asíncronas │ │ │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Cambio | Por qué |
|---|---|
| Turbopack reemplaza Webpack | La arquitectura de Webpack no podía escalar a HMR sub-segundo con más de ~500 módulos. El motor de computación incremental de Turbopack maneja grafos de 10K+ módulos con actualizaciones consistentes de <200ms. |
proxy.ts reemplaza middleware.ts | El middleware mezclaba lógica a nivel de request (auth, redirects) con comportamiento de proxy de red (rewriting, inyección de headers). proxy.ts clarifica qué código corre en el edge vs. el servidor. |
| APIs de request asíncronas | params, searchParams, cookies() y headers() ahora son siempre async. Esto permite mejor streaming y elimina bloqueos implícitos en el rendering de RSC. |
Cache Components (use cache) | Los patrones anteriores de revalidate, unstable_cache y cache() anidados creaban una jerarquía de caché impredecible. use cache provee un primitivo único, declarativo y componible. |
| PPR por defecto | Partial Prerendering ahora es la estrategia de rendering por defecto, combinando shells estáticos con contenido dinámico streameado. Ya no hay que elegir entre SSR y SSG. |
Paso 0: Antes de empezar
1. Registrá tus métricas actuales
Antes de tocar código, capturá tu performance actual:
# Baseline de performance npx next build 2>&1 | tee build-before.log npx @next/bundle-analyzer # Métricas de runtime lighthouse https://your-app.com --output json --output-path baseline.json
2. Verificá la compatibilidad de Node.js
Next.js 16 requiere Node.js 20.x o superior. Node 18 ya no tiene soporte.
node -v # Debe ser >= v20.0.0
3. Ejecutá el comando de upgrade
npx @next/codemod@latest upgrade
Esto maneja la mayor parte de la migración automatizada. Pero no atrapa todo — el resto de esta guía cubre lo que el codemod no toca.
Paso 1: Turbopack — Webpack ya no es el default
El cambio más visible: Turbopack ahora es el bundler por defecto tanto para desarrollo como producción. La clave webpack en next.config.ts está deprecada — aunque si tenés loaders incompatibles, podés hacer fallback temporalmente con next dev --webpack o next build --webpack. Este escape hatch existe para la migración, pero el objetivo es eliminarlo.
¿Qué se rompe?
Si tenés algo de esto en tu next.config.ts, va a fallar:
// ❌ Esto ya no funciona en Next.js 16 module.exports = { webpack: (config) => { config.resolve.alias['@'] = path.resolve(__dirname, 'src'); config.module.rules.push({ test: /\.svg$/, use: ['@svgr/webpack'], }); return config; }, };
Cómo migrar
Para aliases: Usá los paths nativos de tsconfig.json — Turbopack los respeta de forma nativa:
// tsconfig.json { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
Para imports de SVG: Usá @svgr/turbopack en vez de @svgr/webpack:
npm install @svgr/turbopack
// next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { turbopack: { rules: { '*.svg': { loaders: ['@svgr/turbopack'], as: '*.js', }, }, }, }; export default nextConfig;
Para otros loaders custom: Verificá si existe una versión compatible con Turbopack. La mayoría de los loaders populares (CSS modules, SASS, optimización de imágenes) vienen built-in en Turbopack. Para el resto, la config turbopack.rules es tu salida de emergencia.
Verificación
# Desarrollo npx next dev # Si arranca sin errores, Turbopack está funcionando # Build de producción npx next build
Si ves errores Module not found durante el build que no existían antes, es casi seguro un problema de compatibilidad de loaders. Consultá la tabla de compatibilidad de Turbopack para tus loaders específicos.
Paso 2: middleware.ts → proxy.ts
Este es el cambio que genera más confusión. El viejo middleware.ts se dividió en dos capas conceptuales:
proxy.ts— Operaciones a nivel de red (reescritura de URLs, inyección de headers, routing por geolocalización). Corre en el runtime de Node.js por defecto (a diferencia del viejo middleware que defaulteaba a Edge).- Lógica server-side en route handlers — Chequeos de autenticación, validación de sesión y lógica de negocio ahora van en tus route handlers, layouts o server actions.
Cómo era un middleware.ts típico
// ❌ ANTES: middleware.ts (Next.js 15) import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { // Chequeo de auth const token = request.cookies.get('session-token'); if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); } // Detección de locale const locale = request.headers.get('Accept-Language')?.split(',')[0] || 'en'; const response = NextResponse.next(); response.headers.set('x-locale', locale); // Routing de A/B test const bucket = Math.random() > 0.5 ? 'a' : 'b'; response.headers.set('x-ab-bucket', bucket); return response; } export const config = { matcher: ['/((?!api|_next/static|favicon.ico).*)'], };
El nuevo proxy.ts
// ✅ AHORA: proxy.ts (Next.js 16) import type { NextRequest } from 'next/server'; export function proxy(request: NextRequest) { const url = request.nextUrl.clone(); // Detección de locale (concern de red) const locale = request.headers.get('Accept-Language')?.split(',')[0] || 'en'; // Routing de A/B test (concern de red) const bucket = Math.random() > 0.5 ? 'a' : 'b'; return { headers: { 'x-locale': locale, 'x-ab-bucket': bucket, }, }; } export const config = { matcher: ['/((?!api|_next/static|favicon.ico).*)'], };
¿A dónde va la autenticación?
Los chequeos de auth se mueven a los layouts o route handlers donde corresponden:
// app/dashboard/layout.tsx import { redirect } from 'next/navigation'; import { getSession } from '@/lib/auth'; export default async function DashboardLayout({ children, }: { children: React.ReactNode; }) { const session = await getSession(); if (!session) { redirect('/login'); } return <>{children}</>; }
Esto es una mejor arquitectura, en serio. La lógica de auth ahora vive al lado de las rutas que protege, haciendo que el modelo de seguridad sea explícito y auditable en vez de estar escondido en un archivo global de middleware.
Codemod
npx @next/codemod@latest middleware-to-proxy
El codemod maneja patrones simples de headers/rewrites pero no va a mover la lógica de auth automáticamente. Revisá el resultado con cuidado.
Paso 3: APIs de request asíncronas
Todas las APIs de request dinámica ahora son estrictamente async. Este es el cambio con mayor impacto por cantidad de archivos — esperá modificar cada page, layout y route handler que acceda a params, search params, cookies o headers.
Antes (Next.js 15)
// ❌ El acceso sincrónico ya no funciona export default function Page({ params, searchParams, }: { params: { slug: string }; searchParams: { q?: string }; }) { const title = params.slug; const query = searchParams.q; return <div>{title} - {query}</div>; }
Después (Next.js 16)
// ✅ Todas las APIs de request son async export default async function Page({ params, searchParams, }: { params: Promise<{ slug: string }>; searchParams: Promise<{ q?: string }>; }) { const { slug } = await params; const { q } = await searchParams; return <div>{slug} - {q}</div>; }
cookies() y headers()
// ❌ Antes import { cookies, headers } from 'next/headers'; export default function Page() { const cookieStore = cookies(); const headerList = headers(); // ... } // ✅ Después import { cookies, headers } from 'next/headers'; export default async function Page() { const cookieStore = await cookies(); const headerList = await headers(); // ... }
Codemod
npx @next/codemod@latest async-request-apis
Este codemod tiene una tasa de éxito alta (~90%). Agrega async a las funciones, envuelve con await, y actualiza las firmas de tipo. Revisá los resultados — a veces se le escapan edge cases en funciones utilitarias custom que pasan params por referencia.
Paso 4: Cache Components y la directiva use cache
Este es el shift conceptual más grande. El modelo de caché anterior (una mezcla de revalidate, unstable_cache, opciones de caché de fetch y cache()) fue reemplazado por un único primitivo componible: la directiva use cache.
El mundo viejo (confuso)
// ❌ Next.js 15: Múltiples mecanismos de caché superpuestos export const revalidate = 3600; // revalidación a nivel de página async function getData() { const res = await fetch('https://api.example.com/data', { next: { revalidate: 60, tags: ['data'] }, }); return res.json(); } // Más: unstable_cache, cache(), generateStaticParams...
El mundo nuevo (declarativo)
Primero, habilitá Cache Components en tu config:
// next.config.ts const nextConfig: NextConfig = { cacheComponents: true, // Requerido para usar 'use cache' };
Después usá la directiva:
// ✅ Next.js 16: Una sola directiva 'use cache' import { cacheLife, cacheTag } from 'next/cache'; // Caché a nivel de función async function getData() { 'use cache'; cacheLife('hours'); cacheTag('data'); const res = await fetch('https://api.example.com/data'); return res.json(); } // Caché a nivel de componente async function ExpensiveWidget() { 'use cache'; cacheLife('days'); cacheTag('widget'); const data = await getData(); return <div>{data.title}</div>; } // Caché a nivel de página export default async function Page() { 'use cache'; cacheLife('minutes'); return ( <main> <ExpensiveWidget /> <DynamicContent /> </main> ); }
Presets de Cache Life
Next.js 16 viene con presets de tiempo de vida de caché incorporados:
| Preset | Stale | Revalidate | Expire |
|---|---|---|---|
'seconds' | 0s | 1s | 60s |
'minutes' | 5min | 1min | 1hr |
'hours' | 5min | 1hr | 24hr |
'days' | 5min | 1day | 14d |
'weeks' | 5min | 1week | 30d |
'max' | 5min | 30d | 365d |
También podés definir perfiles custom:
// next.config.ts const nextConfig: NextConfig = { cacheLife: { product: { stale: 300, // 5 minutos revalidate: 3600, // 1 hora expire: 86400, // 1 día }, }, };
// Después se usa así: async function getProduct(id: string) { 'use cache'; cacheLife('product'); cacheTag(`product-${id}`); return db.products.findById(id); }
Revalidación
La revalidación por tags funciona igual, pero ahora es más poderosa porque podés taggear a cualquier nivel de granularidad:
// app/api/revalidate/route.ts import { revalidateTag } from 'next/cache'; export async function POST(request: Request) { const { tag } = await request.json(); revalidateTag(tag); return Response.json({ revalidated: true }); }
Estrategia de migración
- Eliminá todos los
export const revalidate = ...de las páginas - Reemplazá
fetch(..., { next: { revalidate } })poruse cache+cacheLife - Reemplazá llamadas a
unstable_cache()por funciones conuse cache - Agregá
cacheTag()donde necesites revalidación dirigida
Paso 5: PPR (Partial Prerendering) por defecto
PPR ahora es la estrategia de rendering por defecto. Esto significa que cada página automáticamente obtiene un shell estático que se sirve al instante, con contenido dinámico streameado vía Suspense boundaries.
Qué significa esto en la práctica
// Esta página automáticamente usa PPR en Next.js 16 export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; return ( <main> {/* Shell estático — servido desde CDN */} <Header /> <ProductInfo id={id} /> {/* Contenido dinámico — streameado */} <Suspense fallback={<PriceSkeleton />}> <DynamicPrice id={id} /> </Suspense> <Suspense fallback={<ReviewsSkeleton />}> <UserReviews id={id} /> </Suspense> </main> ); }
El punto clave: los componentes con use cache se convierten en el shell estático. Los componentes que acceden a cookies, headers o datos no cacheados se convierten en los huecos dinámicos que se streamean.
Si necesitás desactivar PPR
// next.config.ts const nextConfig: NextConfig = { experimental: { ppr: false, // Desactivar PPR globalmente }, };
O por ruta:
// app/legacy-page/page.tsx export const dynamic = 'force-dynamic'; // Esta página no usa PPR
Paso 6: Optimización de memoria para contenedores
Este es el asesino silencioso. El pipeline de rendering RSC más agresivo de Next.js 16 puede consumir significativamente más memoria que v15, especialmente en entornos containerizados con límites estrictos de memoria.
El problema
En deploys de Kubernetes o Docker con límites de memoria (ej: 512MB por pod), podrías ver OOM kills que no pasaban con Next.js 15. La causa raíz es el grafo de módulos en memoria de Turbopack y el motor de rendering RSC manteniendo más estado intermedio.
La solución
// next.config.ts const nextConfig: NextConfig = { turbopack: { memoryLimit: 256 * 1024 * 1024, // 256MB }, experimental: { incrementalCacheHandlerPath: './cache-handler.mjs', }, };
// cache-handler.mjs import { CacheHandler } from '@next/cache-handler-redis'; export default class CustomCacheHandler extends CacheHandler { constructor(options) { super({ ...options, redis: { url: process.env.REDIS_URL, }, inMemoryCacheEnabled: false, }); } }
Recomendaciones de recursos para contenedores
| Tamaño de app | Memoria recomendada | CPU recomendada |
|---|---|---|
| Chica (<50 rutas) | 512MB | 0.5 vCPU |
| Mediana (50-200 rutas) | 1GB | 1 vCPU |
| Grande (200+ rutas) | 2GB | 2 vCPU |
Monitoreá con:
# Vigilar uso de memoria durante el build docker stats --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}" # Perfilar memoria de Node.js durante runtime NODE_OPTIONS="--max-old-space-size=1024 --heapsnapshot-near-heap-limit=3" npm start
Paso 7: Cambios en next.config.ts
Varias opciones de configuración fueron renombradas o reestructuradas:
// next.config.ts — Configuración completa de Next.js 16 import type { NextConfig } from 'next'; const nextConfig: NextConfig = { turbopack: { rules: { '*.svg': { loaders: ['@svgr/turbopack'], as: '*.js', }, }, resolveAlias: { 'legacy-lib': './src/lib/legacy-adapter', }, }, cacheLife: { product: { stale: 300, revalidate: 3600, expire: 86400 }, blog: { stale: 60, revalidate: 900, expire: 86400 }, }, images: { remotePatterns: [ { protocol: 'https', hostname: '**.example.com' }, ], }, async redirects() { return [ { source: '/old-path', destination: '/new-path', permanent: true }, ]; }, }; export default nextConfig;
Opciones eliminadas
Estas opciones de next.config.ts ya no existen:
// ❌ Todo esto fue eliminado en Next.js 16 { webpack: () => {}, // Usar turbopack.rules swcMinify: true, // Siempre activo (Turbopack) experimental: { appDir: true, // Siempre activo desde v14 serverActions: true, // Siempre activo desde v15 typedRoutes: true, // Siempre activo }, }
Checklist completo de migración
Pasá por este checklist después de correr los codemods:
Infraestructura
- Node.js >= 20.x instalado
-
nextactualizado a^16.0.0 -
reactyreact-domactualizados a^19.2.0 - Todos los paquetes
@next/*en versiones que coincidan - Límites de memoria de contenedores revisados (aumentar si < 512MB)
Bundler
- Eliminada toda config
webpackdenext.config.ts - Loaders custom migrados a
turbopack.rules - Paths de
tsconfig.jsonverificados con Turbopack -
npx next buildejecutado sin errores
Routing
- Migrado
middleware.ts→proxy.ts(solo concerns de red) - Lógica de auth movida de middleware a layouts/route handlers
- Patterns del matcher de
proxy.tsrevisados
Data Fetching
- Todos los
paramsysearchParamsson ahoraPromise<T>conawait - Todas las llamadas a
cookies()yheaders()tienenawait - Convertido
export const revalidate→use cache+cacheLife - Habilitado
cacheComponents: trueennext.config.ts - Reemplazado
unstable_cache→ funciones conuse cache - Agregado
cacheTag()para revalidación dirigida
Rendering
- Verificado el comportamiento de PPR con Suspense boundaries
- Agregados componentes skeleton para contenido dinámico
- Testeado
dynamic = 'force-dynamic'donde no se quiera PPR
Producción
- Benchmarkeado el tiempo de build (apuntar a mejora con Turbopack)
- Load-testeado con patrones de tráfico de producción
- Monitoreado uso de memoria en contenedores durante 24 horas
- Verificadas las tasas de cache hit en producción
Timeline real de migración
Basado en migraciones de producción en equipos de diferente tamaño:
| Tamaño de app | Cobertura del codemod | Trabajo manual | Tiempo total |
|---|---|---|---|
| Chica (<50 rutas) | ~85% | 1-2 días | 3-4 días |
| Mediana (50-200 rutas) | ~75% | 3-5 días | 1-2 semanas |
| Grande (200+ rutas) | ~60% | 1-2 semanas | 3-4 semanas |
Lo que más tiempo consume:
- Loaders custom de Webpack → encontrar equivalentes en Turbopack
- Lógica compleja de middleware → descomponer en proxy + auth en layout
- Rediseño de estrategia de caché → mapear patrones viejos de
revalidateause cache
Qué esperar después de la migración
Después de una migración limpia, los equipos típicamente reportan:
- Inicio del dev server: 60-80% más rápido (Turbopack vs Webpack)
- Updates de HMR: 5-10x más rápidos (consistentemente <200ms)
- Build de producción: 20-40% más rápido
- TTFB: 30-50% de mejora con PPR (shell estático servido al instante)
- Uso de memoria: Similar o ligeramente mayor (requiere tuning)
Los gains de performance de Turbopack y PPR por sí solos justifican todo el laburo. La claridad que te dan proxy.ts y use cache es una inversión que paga dividendos a medida que tu app crece.
Next.js 16 es opinado. Te empuja a patrones que son objetivamente mejores, pero te pide trabajo de migración por adelantado. Los codemods resuelven lo mecánico. Los cambios de arquitectura — separar proxy de auth, pasar a caché declarativo, abrazar PPR — esos los tenés que entender vos. Esta guía te dio ambos. A actualizar.
Explora herramientas relacionadas
Prueba estas herramientas gratuitas de Pockit