Guia de migração para o Next.js 16: Turbopack, proxy.ts, Cache Components e todas as breaking changes explicadas
Seu pipeline de CI acabou de quebrar depois de subir o next pra ^16.0.0. O log de erros tem 400 linhas, metade do seu middleware tá quebrado, e apareceu uma nova convenção proxy.ts que você nunca viu na vida. Bem-vindo ao upgrade do Next.js 16.
Next.js 16 é a release mais significativa em termos de arquitetura desde que o App Router chegou na v13. Webpack morreu — Turbopack é o único bundler. A convenção middleware.ts foi substituída por proxy.ts. O modelo de cache inteiro foi reconstruído em torno de Cache Components e da diretiva use cache. E se você tá rodando em containers, tem mudanças de comportamento de memória que vão te pegar em produção se você não souber delas.
Esse guia passa por cada breaking change, explica o porquê, dá os comandos exatos de codemod, e o caminho de migração manual pra quando os codemods não dão conta. Seja um site de marketing pequeno ou uma app enterprise de 200 rotas, esse documento é tudo que você precisa.
O que mudou e por quê
Antes de entrar nos passos de migração, aqui vai o panorama geral do que o Next.js 16 mudou e o motivo por trás de cada decisão:
┌─────────────────────────────────────────────────────────────┐
│ Arquitetura Next.js 16 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Turbopack │ │ proxy.ts │ │ Cache │ │
│ │ (Único │ │ (Substitui │ │ Components │ │
│ │ Bundler) │ │ middleware) │ │ ('use cache') │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬────────┘ │
│ │ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ ┌────────┴────────┐ │
│ │ HMR & Build │ │ Limites de │ │ Cache │ │
│ │ 10x mais │ │ rede claros │ │ declarativo com │ │
│ │ rápido │ │ │ │ TTL granular │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ APIs de │ │ PPR │ │
│ │ Request │ │ (Padrão) │ │
│ │ Assíncronas │ │ │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Mudança | Por quê |
|---|---|
| Turbopack substitui Webpack | A arquitetura do Webpack não conseguia escalar pra HMR sub-segundo com mais de ~500 módulos. O motor de computação incremental do Turbopack lida com grafos de 10K+ módulos com updates consistentes de <200ms. |
proxy.ts substitui middleware.ts | O middleware misturava lógica de request (auth, redirects) com comportamento de proxy de rede (rewriting, injeção de headers). proxy.ts clarifica qual código roda no edge vs. servidor. |
| APIs de request assíncronas | params, searchParams, cookies() e headers() agora são sempre async. Isso permite melhor streaming e elimina bloqueios implícitos no rendering de RSC. |
Cache Components (use cache) | Os padrões anteriores de revalidate, unstable_cache e cache() aninhados criavam uma hierarquia de cache imprevisível. use cache provê uma primitiva única, declarativa e componível. |
| PPR como padrão | Partial Prerendering agora é a estratégia de rendering padrão, combinando shells estáticos com conteúdo dinâmico streameado. Sem mais decidir entre SSR e SSG. |
Passo 0: Antes de começar
1. Registre suas métricas atuais
Antes de mexer em código, capture sua performance atual:
# 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. Verifique a compatibilidade do Node.js
Next.js 16 requer Node.js 20.x ou superior. Node 18 não tem mais suporte.
node -v # Deve ser >= v20.0.0
3. Execute o comando de upgrade
npx @next/codemod@latest upgrade
Isso cuida da maior parte da migração automatizada. Mas não pega tudo — o resto desse guia cobre o que o codemod não toca.
Passo 1: Turbopack — Webpack não é mais o padrão
A mudança mais visível: Turbopack agora é o bundler padrão pra dev e produção. A chave webpack no next.config.ts foi deprecada — mas se você tem loaders incompatíveis, dá pra fazer fallback temporário com next dev --webpack ou next build --webpack. Esse escape hatch existe pra migração, mas o objetivo é eliminá-lo.
O que quebra
Se você tem algo disso no seu next.config.ts, vai falhar:
// ❌ Isso não funciona mais no 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; }, };
Como migrar
Pra aliases: Use os paths nativos do tsconfig.json — Turbopack reconhece nativamente:
// tsconfig.json { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
Pra imports de SVG: Use @svgr/turbopack em 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;
Pra outros loaders custom: Verifique se existe versão compatível com Turbopack. A maioria dos loaders populares (CSS modules, SASS, otimização de imagem) vem built-in no Turbopack. Pro resto, a config turbopack.rules é sua saída de emergência.
Verificação
# Desenvolvimento npx next dev # Se iniciar sem erros, Turbopack tá funcionando # Build de produção npx next build
Se você ver erros Module not found durante o build que não existiam antes, é quase certeza um problema de compatibilidade de loader. Confira a tabela de compatibilidade do Turbopack pros seus loaders específicos.
Passo 2: middleware.ts → proxy.ts
Essa é a mudança que gera mais confusão. O antigo middleware.ts foi dividido em duas camadas conceituais:
proxy.ts— Operações a nível de rede (reescrita de URLs, injeção de headers, roteamento por geolocalização). Roda no runtime Node.js por padrão (diferente do antigo middleware que defaultava pra Edge).- Lógica server-side em route handlers — Checagens de autenticação, validação de sessão e lógica de negócio agora ficam nos seus route handlers, layouts ou server actions.
Como era um 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) { // Checagem de auth const token = request.cookies.get('session-token'); if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); } // Detecção de locale const locale = request.headers.get('Accept-Language')?.split(',')[0] || 'en'; const response = NextResponse.next(); response.headers.set('x-locale', locale); // Roteamento 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).*)'], };
O novo proxy.ts
// ✅ AGORA: proxy.ts (Next.js 16) import type { NextRequest } from 'next/server'; export function proxy(request: NextRequest) { const url = request.nextUrl.clone(); // Detecção de locale (concern de rede) const locale = request.headers.get('Accept-Language')?.split(',')[0] || 'en'; // Roteamento de A/B test (concern de rede) 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).*)'], };
Pra onde vai a autenticação?
As checagens de auth vão pros layouts ou route handlers onde elas pertencem:
// 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}</>; }
Na real, essa é uma arquitetura melhor. A lógica de auth agora vive do lado das rotas que protege, fazendo o modelo de segurança explícito e auditável, em vez de ficar escondido num arquivo global de middleware.
Codemod
npx @next/codemod@latest middleware-to-proxy
O codemod lida com padrões simples de headers/rewrites mas não vai mover lógica de auth automaticamente. Revise o resultado com cuidado.
Passo 3: APIs de request assíncronas
Todas as APIs de request dinâmica agora são estritamente async. Essa é a mudança com maior impacto em número de arquivos — espere modificar cada page, layout e route handler que acessa params, search params, cookies ou headers.
Antes (Next.js 15)
// ❌ Acesso síncrono não funciona mais 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>; }
Depois (Next.js 16)
// ✅ Todas as APIs de request são 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() e headers()
// ❌ Antes import { cookies, headers } from 'next/headers'; export default function Page() { const cookieStore = cookies(); const headerList = headers(); // ... } // ✅ Depois 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
Esse codemod tem taxa de sucesso alta (~90%). Adiciona async nas funções, envolve com await, e atualiza as assinaturas de tipo. Confira os resultados — às vezes ele perde edge cases em funções utilitárias custom que passam params por referência.
Passo 4: Cache Components e a diretiva use cache
Essa é a maior mudança conceitual. O modelo de cache anterior (uma mistura de revalidate, unstable_cache, opções de cache do fetch e cache()) foi substituído por uma única primitiva componível: a diretiva use cache.
O mundo antigo (confuso)
// ❌ Next.js 15: Múltiplos mecanismos de cache sobrepostos export const revalidate = 3600; // revalidação a nível de página async function getData() { const res = await fetch('https://api.example.com/data', { next: { revalidate: 60, tags: ['data'] }, }); return res.json(); } // Mais: unstable_cache, cache(), generateStaticParams...
O mundo novo (declarativo)
Primeiro, habilite Cache Components na sua config:
// next.config.ts const nextConfig: NextConfig = { cacheComponents: true, // Obrigatório pra usar 'use cache' };
Depois use a diretiva:
// ✅ Next.js 16: Uma única diretiva 'use cache' import { cacheLife, cacheTag } from 'next/cache'; // Cache a nível de função async function getData() { 'use cache'; cacheLife('hours'); cacheTag('data'); const res = await fetch('https://api.example.com/data'); return res.json(); } // Cache a nível de componente async function ExpensiveWidget() { 'use cache'; cacheLife('days'); cacheTag('widget'); const data = await getData(); return <div>{data.title}</div>; } // Cache a nível de página export default async function Page() { 'use cache'; cacheLife('minutes'); return ( <main> <ExpensiveWidget /> <DynamicContent /> </main> ); }
Presets de Cache Life
Next.js 16 vem com presets de tempo de vida de cache embutidos:
| 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 |
Você também pode definir perfis custom:
// next.config.ts const nextConfig: NextConfig = { cacheLife: { product: { stale: 300, // 5 minutos revalidate: 3600, // 1 hora expire: 86400, // 1 dia }, }, };
// Aí usa assim: async function getProduct(id: string) { 'use cache'; cacheLife('product'); cacheTag(`product-${id}`); return db.products.findById(id); }
Revalidação
A revalidação por tags funciona do mesmo jeito, mas agora é mais poderosa porque você pode taggear em qualquer nível de granularidade:
// 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 }); }
Estratégia de migração
- Remova todos os
export const revalidate = ...das páginas - Substitua
fetch(..., { next: { revalidate } })poruse cache+cacheLife - Substitua chamadas de
unstable_cache()por funções comuse cache - Adicione
cacheTag()onde precisar de revalidação direcionada
Passo 5: PPR (Partial Prerendering) como padrão
PPR agora é a estratégia de rendering padrão. Isso significa que toda página automaticamente ganha um shell estático servido instantaneamente, com conteúdo dinâmico streameado via Suspense boundaries.
O que isso significa na prática
// Essa página automaticamente usa PPR no Next.js 16 export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; return ( <main> {/* Shell estático — servido da CDN */} <Header /> <ProductInfo id={id} /> {/* Conteúdo dinâmico — streameado */} <Suspense fallback={<PriceSkeleton />}> <DynamicPrice id={id} /> </Suspense> <Suspense fallback={<ReviewsSkeleton />}> <UserReviews id={id} /> </Suspense> </main> ); }
O ponto chave: componentes com use cache viram o shell estático. Componentes que acessam cookies, headers ou dados não cacheados viram os buracos dinâmicos que são streameados.
Se precisar desativar PPR
// next.config.ts const nextConfig: NextConfig = { experimental: { ppr: false, // Desativar PPR globalmente }, };
Ou por rota:
// app/legacy-page/page.tsx export const dynamic = 'force-dynamic'; // Essa página não usa PPR
Passo 6: Otimização de memória pra containers
Esse é o assassino silencioso. O pipeline de rendering RSC mais agressivo do Next.js 16 pode consumir significativamente mais memória que o v15, especialmente em ambientes containerizados com limites rígidos de memória.
O problema
Em deploys de Kubernetes ou Docker com limites de memória (ex: 512MB por pod), você pode ver OOM kills que não aconteciam no Next.js 15. A causa raiz é o grafo de módulos em memória do Turbopack e o motor de rendering RSC mantendo mais estado intermediário.
A solução
// 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, }); } }
Recomendações de recursos pra containers
| Tamanho da app | Memória recomendada | CPU recomendada |
|---|---|---|
| Pequena (<50 rotas) | 512MB | 0.5 vCPU |
| Média (50-200 rotas) | 1GB | 1 vCPU |
| Grande (200+ rotas) | 2GB | 2 vCPU |
Monitore com:
# Vigiar uso de memória durante build docker stats --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}" # Perfilar memória do Node.js durante runtime NODE_OPTIONS="--max-old-space-size=1024 --heapsnapshot-near-heap-limit=3" npm start
Passo 7: Mudanças no next.config.ts
Várias opções de configuração foram renomeadas ou reestruturadas:
// next.config.ts — Configuração completa do 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;
Opções removidas
Essas opções do next.config.ts não existem mais:
// ❌ Tudo isso foi removido no Next.js 16 { webpack: () => {}, // Usar turbopack.rules swcMinify: true, // Sempre ativo (Turbopack) experimental: { appDir: true, // Sempre ativo desde v14 serverActions: true, // Sempre ativo desde v15 typedRoutes: true, // Sempre ativo }, }
Checklist completo de migração
Passe por esse checklist depois de rodar os codemods:
Infraestrutura
- Node.js >= 20.x instalado
-
nextatualizado pra^16.0.0 -
reactereact-domatualizados pra^19.2.0 - Todos os pacotes
@next/*em versões compatíveis - Limites de memória dos containers revisados (aumentar se < 512MB)
Bundler
- Removida toda config
webpackdonext.config.ts - Loaders custom migrados pra
turbopack.rules - Paths do
tsconfig.jsonverificados com Turbopack -
npx next buildexecutado sem erros
Routing
- Migrado
middleware.ts→proxy.ts(só concerns de rede) - Lógica de auth movida de middleware pra layouts/route handlers
- Patterns do matcher do
proxy.tsrevisados
Data Fetching
- Todos os
paramsesearchParamsagora sãoPromise<T>comawait - Todas as chamadas de
cookies()eheaders()têmawait - Convertido
export const revalidate→use cache+cacheLife - Habilitado
cacheComponents: truenonext.config.ts - Substituído
unstable_cache→ funções comuse cache - Adicionado
cacheTag()pra revalidação direcionada
Rendering
- Verificado o comportamento do PPR com Suspense boundaries
- Adicionados componentes skeleton pro conteúdo dinâmico
- Testado
dynamic = 'force-dynamic'onde PPR não é desejado
Produção
- Benchmarkado o tempo de build (esperar melhora com Turbopack)
- Load-testado com padrões de tráfego de produção
- Monitorado uso de memória em containers por 24 horas
- Verificadas as taxas de cache hit em produção
Timeline real de migração
Baseado em migrações de produção em times de diferentes tamanhos:
| Tamanho da app | Cobertura do codemod | Trabalho manual | Tempo total |
|---|---|---|---|
| Pequena (<50 rotas) | ~85% | 1-2 dias | 3-4 dias |
| Média (50-200 rotas) | ~75% | 3-5 dias | 1-2 semanas |
| Grande (200+ rotas) | ~60% | 1-2 semanas | 3-4 semanas |
O que consome mais tempo:
- Loaders custom de Webpack → encontrar equivalentes no Turbopack
- Lógica complexa de middleware → decompor em proxy + auth no layout
- Redesign da estratégia de cache → mapear padrões antigos de
revalidateprause cache
O que esperar depois da migração
Depois de uma migração limpa, times tipicamente reportam:
- Início do dev server: 60-80% mais rápido (Turbopack vs Webpack)
- Updates de HMR: 5-10x mais rápidos (consistentemente <200ms)
- Build de produção: 20-40% mais rápido
- TTFB: 30-50% de melhora com PPR (shell estático servido instantaneamente)
- Uso de memória: Similar ou ligeiramente maior (requer tuning)
Os ganhos de performance do Turbopack e PPR sozinhos justificam o esforço de migração. A clareza arquitetural do proxy.ts e use cache é um investimento de longo prazo que dá dividendos conforme sua app cresce.
Next.js 16 é opinativo. Te força pra padrões que são objetivamente melhores mas requerem trabalho de migração antecipado. Os codemods cuidam das mudanças mecânicas. As mudanças arquiteturais — separar proxy de auth, adotar cache declarativo, abraçar PPR — essas requerem entendimento. Esse guia te deu os dois. Bora atualizar.
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit