Back

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   │  │              │                         │
│  └──────────────┘  └──────────────┘                         │
└─────────────────────────────────────────────────────────────┘
CambioPor qué
Turbopack reemplaza WebpackLa 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.tsEl 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íncronasparams, 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 defectoPartial 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:

  1. 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).
  2. 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:

PresetStaleRevalidateExpire
'seconds'0s1s60s
'minutes'5min1min1hr
'hours'5min1hr24hr
'days'5min1day14d
'weeks'5min1week30d
'max'5min30d365d

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

  1. Eliminá todos los export const revalidate = ... de las páginas
  2. Reemplazá fetch(..., { next: { revalidate } }) por use cache + cacheLife
  3. Reemplazá llamadas a unstable_cache() por funciones con use cache
  4. 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 appMemoria recomendadaCPU recomendada
Chica (<50 rutas)512MB0.5 vCPU
Mediana (50-200 rutas)1GB1 vCPU
Grande (200+ rutas)2GB2 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
  • next actualizado a ^16.0.0
  • react y react-dom actualizados 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 webpack de next.config.ts
  • Loaders custom migrados a turbopack.rules
  • Paths de tsconfig.json verificados con Turbopack
  • npx next build ejecutado sin errores

Routing

  • Migrado middleware.tsproxy.ts (solo concerns de red)
  • Lógica de auth movida de middleware a layouts/route handlers
  • Patterns del matcher de proxy.ts revisados

Data Fetching

  • Todos los params y searchParams son ahora Promise<T> con await
  • Todas las llamadas a cookies() y headers() tienen await
  • Convertido export const revalidateuse cache + cacheLife
  • Habilitado cacheComponents: true en next.config.ts
  • Reemplazado unstable_cache → funciones con use 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 appCobertura del codemodTrabajo manualTiempo total
Chica (<50 rutas)~85%1-2 días3-4 días
Mediana (50-200 rutas)~75%3-5 días1-2 semanas
Grande (200+ rutas)~60%1-2 semanas3-4 semanas

Lo que más tiempo consume:

  1. Loaders custom de Webpack → encontrar equivalentes en Turbopack
  2. Lógica compleja de middleware → descomponer en proxy + auth en layout
  3. Rediseño de estrategia de caché → mapear patrones viejos de revalidate a use 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.

Next.jsNext.js 16migraciónTurbopackReactTypeScriptdesarrollo webfrontendproxycache componentsPPR

Explora herramientas relacionadas

Prueba estas herramientas gratuitas de Pockit