Guia prático para migrar do Pages Router para o App Router no Next.js: estratégia gradual e armadilhas

O diretor de tecnologia lançou uma pergunta na sala de reunião: “Dá para atualizar este nosso projeto em Next.js 12 para a versão 14?”
Olhei para aquele projeto antigo na tela, em execução havia dois anos. Na última atualização, para o React 17, passamos uma semana inteira corrigindo bugs, e o telefone do atendimento ao cliente não parava de tocar.
Mas, desta vez, a situação parecia um pouco diferente. À noite, em casa, consultei a documentação oficial e vi os novos recursos do App Router: Server Components, layouts aninhados, melhor desempenho… Fiquei com vontade de testar. Quando cheguei à página do guia de migração, porém, bateu a preocupação: a tela inteira estava tomada por tabelas de equivalência entre APIs. O que deveria substituir getServerSideProps? Em que transformar o _app.js? Era muita coisa.
Para piorar, a “migração gradual” recomendada oficialmente parece ótima na teoria, mas, quando testei de verdade, descobri que o usuário vê um indicador de loading ao navegar entre /pages e /app. A experiência ficou ainda pior.
Passei duas semanas inteiras enfrentando problemas, lendo discussões da comunidade e testando abordagens diferentes. No fim, cheguei a uma estratégia de migração relativamente confiável. Neste artigo, compartilho essa experiência prática, incluindo:
- Como decidir se vale a pena migrar seu projeto
- As vantagens e desvantagens reais de duas estratégias de migração, para além da teoria da documentação oficial
- Etapas detalhadas e exemplos de código para migrar getServerSideProps
- Sete grandes armadilhas que enfrentei pessoalmente e como resolvê-las
Se você também está em dúvida sobre a atualização ou já começou a migração e encontrou problemas, espero que este artigo ajude a evitar alguns desvios.
Por que migrar? Faça as contas primeiro
Antes de falar da migração, preciso jogar um pouco de água fria: nem todo projeto merece esse esforço.
No mês passado, um amigo me perguntou se deveria atualizar para o App Router uma landing page de campanha que a empresa dele desativaria em breve. Recomendei que não fizesse isso. Se o código será excluído em seis meses, por que desperdiçar tempo?
Então, que tipo de projeto vale a pena migrar? Reuni alguns critérios para ajudar nessa decisão.
Necessidade de layouts aninhados
Esse foi o principal motivo da migração da nossa equipe. O painel do nosso SaaS tinha um layout de três níveis, com barra lateral, navegação superior e área de conteúdo. No Pages Router, a barra lateral inteira era renderizada novamente a cada mudança de página.
Ao abrir uma nova página, o usuário percebia claramente uma piscada na interface. Não era lentidão da rede, mas uma nova renderização do layout.
Os layouts aninhados do App Router resolveram o problema perfeitamente. Depois da migração, a equipe da WorkOS relatou que “a experiência de login melhorou significativamente, sem estados de carregamento nem deslocamentos de layout”. Nossos testes chegaram à mesma conclusão: quando o usuário troca de página, apenas a área de conteúdo é atualizada, enquanto a navegação permanece totalmente estável.
Espaço para otimizar o desempenho
Se a primeira renderização do seu projeto demora mais de 3 segundos, o App Router pode ajudar.
Tínhamos uma página de lista de produtos que usava getServerSideProps para buscar dados. A cada atualização, era preciso esperar o servidor renderizar todo o HTML. Depois de migrar para Server Components, pudemos buscar os dados da lista no servidor e enviá-los diretamente ao cliente por streaming. O tempo de carregamento inicial caiu de 3,2 para 1,8 segundo.
Mas há uma ressalva: nem toda página ficará mais rápida. Em páginas com interação totalmente no cliente, como um editor de canvas, a migração praticamente não faz diferença e pode até deixá-las mais lentas devido a uma camada adicional de abstração.
Projetos com manutenção no longo prazo
Se o projeto continuará em manutenção por mais de três anos, vale aproveitar os benefícios da migração o quanto antes. A Vercel já deixou claro que os novos recursos terão suporte prioritário no App Router, enquanto o Pages Router entrou em “modo de manutenção”.
Eu não queria ter de migrar tudo daqui a dois anos, quando as APIs talvez tivessem mudado novamente e as armadilhas fossem ainda mais numerosas.
Quando não recomendo migrar
Nestes casos, recomendo não ter pressa:
- O projeto será desativado em breve — não vale a pena
- Site estático pequeno, com até 5 páginas — o retorno é baixo demais
- A equipe não domina React 18 — entenda Suspense e Server Components primeiro
- O projeto depende de muitas bibliotecas de terceiros antigas — você pode encontrar uma longa lista de incompatibilidades
Depois de tudo isso, a ideia central cabe em uma frase: não migre apenas por migrar. Primeiro, pergunte qual problema real a migração resolverá. Se a resposta for “nenhum, só quero testar uma novidade”, é melhor deixar como está.
Nossa equipe fez as contas: duas semanas de trabalho em troca de uma experiência melhor para o usuário e menos dívida técnica nos próximos três anos. Para nós, valeu a pena. E para seu projeto?
Como escolher entre duas estratégias de migração
A documentação oficial recomenda a “migração gradual”. Parece uma abordagem segura: avançar devagar, uma página de cada vez.
Depois de testar, porém, descobri um problema fatal nessa estratégia.
A armadilha da migração gradual
Imagine este cenário: você migrou a página inicial para o diretório /app, mas a página de detalhes do produto ainda está em /pages. O usuário clica para abrir o produto, a tela fica branca e aparece um indicador de loading. Só depois o conteúdo é exibido.
Por quê? Ao sair do App Router para o Pages Router, o Next.js trata os dois como aplicações independentes e precisa recarregar todo o bundle JavaScript. A experiência do usuário volta instantaneamente a 2010.
A equipe da WorkOS também reclamou disso em seu blog: “Navegar entre roteadores diferentes é como alternar entre duas aplicações sem relação entre si”. A intenção inicial deles era fazer uma migração gradual, mas acabaram desistindo.
Isso significa que a migração gradual é totalmente inviável? Não.
Cenários adequados para uma migração gradual:
- As páginas têm pouco acoplamento entre si, como em um blog no qual os artigos são independentes
- É possível migrar módulos inteiros, por exemplo, migrar primeiro toda a área do usuário e depois o módulo de produtos
- O estado de loading durante a alternância é aceitável
Já vi um blog técnico seguir essa abordagem com um resultado razoável. Para um SaaS ou e-commerce, porém, é melhor nem pensar nisso.
A solução sem indisponibilidade da WorkOS
E o que fazer em um projeto complexo? A WorkOS apresentou uma solução engenhosa.
A equipe criou um diretório temporário /app/new dentro de /app, reescreveu ali todas as páginas e passou a usar um parâmetro de consulta para controlar qual versão seria acessada.
Pode parecer complicado, mas o código esclarece a ideia:
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/:path*',
destination: '/new/:path*',
has: [
{
type: 'query',
key: 'new',
value: 'true',
},
],
},
]
},
}
Com isso, um usuário comum que acessa /dashboard continua usando a versão antiga. Ao acrescentar ?new=true, ele vê a nova versão.
As equipes de testes, produto e design conseguem validar antecipadamente a nova versão no ambiente de produção, sem qualquer impacto para os usuários. Quando todos os testes terminam, basta mover o conteúdo de /app/new para /app, excluir /pages e encerrar a migração.
Foi exatamente a solução adotada pela nossa equipe. Durante todo o processo, nenhum usuário encontrou sequer um bug, porque testamos a nova versão com dados reais durante uma semana antes da entrada oficial em produção.
Etapas detalhadas:
- Atualize o Next.js para a versão 14 — primeiro, atualize apenas o framework, sem mexer em
/pages - Migre os hooks de roteamento — troque
next/routerpornext/navigatione garanta que o código seja compatível com os dois roteadores - Crie o diretório
/app/new— reconstrua ali a estrutura das páginas - Reutilize os componentes existentes — importe diretamente os componentes React de
/pages, sem reescrevê-los - Configure rewrites — adicione a configuração acima e use
?new=truepara alternar a versão - Faça testes internos e uma liberação gradual — permita que a equipe use a nova versão e corrija rapidamente os problemas encontrados
- Coloque em produção — mova
/app/newpara/appe exclua rewrites e/pages
Da primeira à sétima etapa, gastamos 10 dias úteis: 6 dias para reescrever páginas, 3 para corrigir bugs e o último para colocar a nova versão em produção.
Minha recomendação
Se seu projeto tem:
- Menos de 10 páginas independentes → migração gradual
- Mais de 10 páginas e alta exigência de experiência do usuário → opção sem indisponibilidade
- É um projeto novo → use diretamente o App Router e evite esse trabalho
Não tente migrar enquanto desenvolve novas funcionalidades. Eu já fiz isso, e o resultado foram dois lados do projeto com estilos de código completamente diferentes e desagradáveis de manter. Reserve duas semanas e termine o trabalho de uma vez ou não mexa nele por enquanto.
Migração prática de getServerSideProps
Esta é a pergunta que mais recebo: “Se getServerSideProps não pode mais ser usado, como buscar os dados?”
Na verdade, a obtenção de dados no App Router é mais simples. Só é preciso mudar a forma de pensar.
Da “separação” para a “integração”
No Pages Router, a lógica funciona assim: a obtenção dos dados, feita por getServerSideProps, fica separada da UI, que está no componente. O Next.js chama a função de dados no servidor e repassa o resultado ao componente.
O App Router abandona esse modelo. O próprio componente da página é uma função async que busca os dados diretamente:
// ❌ Forma antiga: pages/project/[id].tsx
export async function getServerSideProps(context) {
const { id } = context.params
const res = await fetch(`https://api.example.com/projects/${id}`)
const project = await res.json()
return {
props: { project }
}
}
export default function ProjectPage({ project }) {
return <h1>{project.title}</h1>
}
// ✅ Forma nova: app/project/[id]/page.tsx
export default async function ProjectPage({ params }) {
const { id } = params
const res = await fetch(`https://api.example.com/projects/${id}`, {
cache: 'no-store' // Essencial! Equivale ao comportamento de getServerSideProps
})
const project = await res.json()
return <h1>{project.title}</h1>
}
Parece bem mais simples, certo? Mas não tenha pressa: há duas grandes armadilhas aqui.
Armadilha 1: configuração de cache incorreta
Por padrão, o fetch no App Router usa cache, de modo equivalente a getStaticProps, em vez de buscar dados novos a cada requisição.
No início da migração, não percebi isso. Migrei uma página de preços de produtos e os valores pararam de ser atualizados. Os usuários reclamavam: “O preço caiu, mas a página ainda mostra o valor antigo”. Passei um bom tempo investigando até descobrir que o cache era o culpado.
Guarde esta tabela de equivalência:
getServerSideProps→cache: 'no-store'getStaticProps→cache: 'force-cache', o comportamento padrãogetStaticProps + revalidate→next: { revalidate: 60 }
Armadilha 2: o que fazer com o estado do cliente
Uma página que usava getServerSideProps costuma ter também interações no cliente, como filtros e ordenação.
Depois de migrar para o App Router, você descobre que um Server Component assíncrono não pode usar hooks como useState e useEffect.
O que fazer? Separe os componentes.
// app/products/page.tsx (Server Component)
export default async function ProductsPage() {
const products = await fetchProducts() // Busca os dados no servidor
return <ProductList initialData={products} /> // Repassa ao Client Component
}
// components/ProductList.tsx (Client Component)
'use client' // Observe esta linha!
import { useState } from 'react'
export function ProductList({ initialData }) {
const [products, setProducts] = useState(initialData)
const [filter, setFilter] = useState('')
// Lógica de filtro no cliente
const filtered = products.filter(p => p.name.includes(filter))
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</div>
)
}
Assim, o servidor fica responsável pela obtenção dos dados e o cliente, pelas interações. As responsabilidades ficam bem definidas.
Mas atenção: não abuse de 'use client'. Já vi gente marcar a página inteira com 'use client', eliminando todo o sentido dos Server Components.
Etapas práticas da migração
Resumi o processo em duas etapas:
Primeira etapa: separe os componentes
Ainda no diretório pages original, divida o componente entre uma parte de apresentação pura e outra com estado. Teste antes de avançar.
Segunda etapa: mova para o diretório app
- Coloque a parte de apresentação em
app/[route]/page.tsx, marque-a como async e busque os dados ali - Extraia a parte com estado para um arquivo separado e adicione
'use client' - Exclua o código de getServerSideProps
A vantagem dessa abordagem é que, se algo der errado, você poderá reverter rapidamente sem bagunçar o código dos dois lados.
Mais um pequeno detalhe
Se antes você lia a identidade do usuário com context.req.cookies, passe a usar:
import { cookies } from 'next/headers'
export default async function Page() {
const cookieStore = cookies()
const token = cookieStore.get('auth-token')
// Use o token para buscar os dados do usuário...
}
Recursos semelhantes, como headers() e redirect(), também são importados de next/headers ou next/navigation. A documentação oficial tem a lista completa, então não vou reproduzi-la aqui.
Sete armadilhas comuns e suas soluções
Agora chegamos à parte principal. Enfrentei pessoalmente as sete armadilhas abaixo, e cada uma me custou pelo menos uma hora de debugging.
Armadilha 1: erros do servidor ficam ocultos
Sintoma: a página não é renderizada nem exibe erro. Aparece apenas um skeleton de loading ou uma tela em branco.
Minha experiência: certa vez, alterei uma chamada de API e a página ficou completamente vazia. Abri o console e não havia erro algum. Imaginei que os dados não tivessem voltado, adicionei vários console.log e, mesmo assim, não encontrei o problema.
No fim, descobri que o servidor havia lançado uma exceção. Como eu não tinha configurado error.tsx, o Next.js ocultou o erro e mostrou diretamente o fallback do Suspense.
Solução: adicione um error.tsx a cada diretório de rota:
// app/dashboard/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<h2>Ocorreu um erro: {error.message}</h2>
<button onClick={reset}>Tentar novamente</button>
</div>
)
}
Com isso, pelo menos você consegue ver a mensagem de erro. No ambiente de desenvolvimento, o Next.js mostra a stack detalhada; em produção, exibe uma mensagem amigável.
Armadilha 2: useRouter parou de funcionar
Sintoma: useRouter().push() não navega, ou aparece um erro informando que determinado método não existe.
Causa: next/router e next/navigation são duas APIs distintas e incompatíveis.
No início, imaginei que bastaria mudar o caminho do import:
// ❌ Forma incorreta
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push('/dashboard') // O método push não existe!
Só depois descobri que useRouter em next/navigation não tinha o método push e que seria preciso usar uma função separada:
// ✅ Forma correta
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
const router = useRouter()
router.push('/dashboard') // Na verdade, o método existe, mas seu comportamento é diferente
// Ou use diretamente o componente Link
import Link from 'next/link'
<Link href="/dashboard">Acessar</Link>
Tabela de equivalência, que deixei ao lado do computador durante a migração:
| Pages Router | App Router |
|---|---|
useRouter().push(url) | useRouter().push(url) (existe, mas não é recomendado) |
useRouter().pathname | usePathname() |
useRouter().query | useSearchParams() |
useRouter().asPath | usePathname() + useSearchParams() |
Armadilha 3: imports dinâmicos deixam de funcionar
Sintoma: um componente importado com next/dynamic não é renderizado, e o console exibe “You’re importing a component that needs useState. It only works in a Client Component…”.
Causa: Server Components são renderizados no servidor por padrão, e algumas bibliotecas client-only, como as bibliotecas de gráficos, geram erros.
Eu tinha uma página de gráficos que usava ECharts:
// ❌ Isto gera um erro
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false })
export default function Page() {
return <Chart data={data} />
}
O erro dizia que o componente Chart precisava do objeto window, que não existe no servidor.
Solução: adicione 'use client' ao page.tsx ou extraia Chart para um Client Component separado:
// app/charts/page.tsx
import { ClientChart } from './ClientChart'
export default function Page() {
return <ClientChart />
}
// app/charts/ClientChart.tsx
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false })
export function ClientChart() {
return <Chart data={data} />
}
Armadilha 4: a página pisca durante a navegação
Sintoma: ao clicar em um link, a página inteira é renderizada novamente, e a barra de navegação superior e a barra lateral piscam.
Causa: o layout.tsx foi configurado incorretamente ou não foi usado.
A principal vantagem do App Router são os layouts aninhados. No início, não aproveitei bem esse recurso e inseri a barra de navegação diretamente em cada page.tsx. É claro que ela piscava.
Forma correta:
// app/layout.tsx (layout raiz, compartilhado por todas as páginas)
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header /> {/* Navegação superior, nunca renderizada novamente */}
{children}
</body>
</html>
)
}
// app/dashboard/layout.tsx (layout do dashboard)
export default function DashboardLayout({ children }) {
return (
<div className="flex">
<Sidebar /> {/* Barra lateral, não renderizada novamente ao navegar dentro do dashboard */}
<main>{children}</main>
</div>
)
}
Assim, quando o usuário navega entre /dashboard/analytics e /dashboard/settings, somente a área main é atualizada, enquanto a barra lateral permanece imóvel.
Armadilha 5: a página 404 não funciona
Sintoma: a página 404 personalizada não aparece; o Next.js continua exibindo a padrão.
Causa: o 404.js do Pages Router entra em conflito com o not-found.tsx do App Router.
Durante minha migração, /pages/404.js ainda existia e impedia o funcionamento de /app/not-found.tsx.
Solução: exclua /pages/404.js e /pages/500.js e use as convenções do App Router:
// app/not-found.tsx
export default function NotFound() {
return <h1>Página não encontrada</h1>
}
Para disparar manualmente um 404 em page.tsx:
import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const data = await fetchData(params.id)
if (!data) {
notFound() // Dispara o 404
}
return <div>{data.title}</div>
}
Armadilha 6: o servidor de desenvolvimento fica cada vez mais lento
Sintoma: logo depois de iniciar, tudo funciona bem. Após algumas alterações, o hot reload leva 10 segundos e, mais tarde, o servidor trava.
Sendo sincero: também não encontrei uma solução perfeita.
Esse é um problema conhecido do Next.js 14. A equipe da FlightControl reclamou em seu blog: “O desempenho do dev server é tão ruim que eu abriria mão de todos os novos recursos para evitá-lo”. Segundo eles, era preciso reiniciar o servidor de desenvolvimento a cada 20 minutos.
A experiência da nossa equipe foi parecida.
Soluções temporárias:
- Reinicie o dev server periodicamente; configurei um lembrete para cada 15 minutos
- Use
next dev --turbopara ativar o Turbopack experimental; ele é mais rápido, mas apresenta bugs ocasionalmente - Reduza Server Components desnecessários; em algumas páginas, um Client Component é suficiente
Dizem que o Next.js 15 melhorou esse problema, mas ainda não testei.
Armadilha 7: bibliotecas de terceiros incompatíveis
Sintoma: algumas bibliotecas de animação, como Framer Motion e Lottie, geram erros dizendo que window ou document não existem.
Causa: essas bibliotecas funcionam apenas no cliente e não podem ser usadas em um Server Component.
Antes, eu usava Framer Motion para animar transições entre páginas. Depois da migração, tudo parou de funcionar.
Solução:
- Envolva os componentes que usam essas bibliotecas com
'use client' - Verifique se existe uma versão mais recente da biblioteca com suporte a React 18; algumas já foram adaptadas
- Se nada funcionar, troque-a por uma biblioteca com suporte a SSR
Tenha atenção especial a estas bibliotecas client-only comuns:
- Framer Motion; as animações de saída de página apresentam problemas no App Router, ainda discutidos em uma issue oficial
- Bibliotecas de carrossel, como swiper e slick-carousel
- Bibliotecas de gráficos, como ECharts e Chart.js
- Bibliotecas de arrastar e soltar, como react-dnd e dnd-kit
Se o projeto depende muito dessas bibliotecas, consulte as issues no GitHub antes da migração e confirme a compatibilidade.
Recomendações de otimização após a migração
Concluir a migração não é o fim. Ainda há bastante espaço para otimizar.
Reduza o JavaScript no cliente
Esta é a maior vantagem dos Server Components.
Na nossa antiga página de lista de produtos, só os componentes React ocupavam 120 KB depois de gzip. Após a migração, transformamos a parte de exibição dos dados em um Server Component e mantivemos apenas os filtros e a ordenação em um Client Component. O bundle caiu para 45 KB.
Como verificar:
npm run build
Observe na saída quais páginas aparecem como (Static) ou (SSR) e quais têm ○, indicando o uso de Client Component. Se a tela estiver cheia de ○, talvez você tenha exagerado no uso de 'use client'.
Dicas de otimização:
- Coloque conteúdo estático, como texto e imagens, em Server Components
- Use Client Components para componentes interativos, como formulários e botões
- Não marque a página inteira com
'use client'; marque apenas os componentes filhos que realmente precisam disso
Use o cache de forma adequada
A estratégia de cache do App Router é muito mais complexa que a do Pages Router.
// Sem cache: busca dados novos em cada requisição, indicado para dados em tempo real
fetch(url, { cache: 'no-store' })
// Mantém o cache por 60 segundos e depois revalida, indicado para dados atualizados com frequência, mas que não precisam ser instantâneos
fetch(url, { next: { revalidate: 60 } })
// Cache permanente, indicado para dados estáticos que não mudam
fetch(url, { cache: 'force-cache' })
Nossa lista de produtos usou revalidate de 60 segundos. Assim, os dados não ficavam muito desatualizados e a carga sobre o servidor diminuía. Depois da entrada em produção, o volume de chamadas à API caiu 60%.
Monitore o desempenho
Compare estas métricas antes e depois da migração:
- First Contentful Paint (FCP) — tempo até o usuário ver o primeiro conteúdo
- Time to Interactive (TTI) — tempo até a página ficar totalmente interativa
- Cumulative Layout Shift (CLS) — indica se o layout se desloca durante o carregamento
Usamos o Vercel Analytics para monitorar o resultado. Após a migração, o FCP caiu de 3,2 para 1,8 segundo, e o TTI, de 5,1 para 3,3 segundos.
Mas lembre-se de que nem toda página ficará mais rápida. Em páginas com interação totalmente no cliente, como nosso editor de canvas, a migração praticamente não fez diferença.
Cuidado com o excesso de otimização
Não divida componentes à força apenas para usar Server Components.
Certa vez, dividi um formulário em 20 componentes pequenos porque pensei que, quanto mais fragmentado, maior seria a participação de Server Components. O resultado foi um código menos legível, difícil de entender para os colegas e com custo de manutenção maior.
Regra prática: se um componente precisa de useState ou useEffect, marque-o diretamente com 'use client' e não complique. Server Components são uma ferramenta, não um KPI.
Conclusão
Depois de tudo isso, estes são os pontos centrais:
Não migre apenas para acompanhar uma tendência, mas para resolver problemas reais. Se seu projeto precisa de layouts aninhados, quer reduzir o JavaScript no cliente ou terá manutenção de longo prazo, vale investir no App Router.
Escolha a estratégia certa: em projetos pequenos, avance gradualmente; em projetos grandes, use a opção sem indisponibilidade e faça a troca de uma só vez. Não tente desenvolver novas funcionalidades e migrar ao mesmo tempo, porque o processo ficará confuso.
Encontrar armadilhas é normal. As sete que listei são apenas a ponta do iceberg. Quando surgir um problema, pesquise primeiro nas issues do GitHub; em 90% dos casos, alguém já enfrentou a mesma situação.
Não otimize demais. Server Components são uma ferramenta, não o objetivo. A legibilidade do código e a eficiência da equipe são mais importantes que o tamanho do bundle.
Minha recomendação é começar com 1 ou 2 páginas piloto, validar todo o processo, reunir os aprendizados e só então avançar para o restante. Foi assim que nossa equipe trabalhou: levamos 3 dias na primeira página e, depois de entender o processo, apenas 5 dias nas 10 páginas seguintes.
Por fim, o App Router do Next.js realmente tem vários problemas, especialmente no desempenho do dev server, mas a direção geral está correta. À medida que o ecossistema amadurecer, essas lacunas serão preenchidas.
Se você também encontrou problemas durante a migração, compartilhe sua experiência nos comentários. Talvez eu já tenha passado pela mesma situação.
Recursos relacionados:
- Guia oficial de migração do Next.js
- Estratégia de migração sem indisponibilidade da WorkOS
- Problemas comuns do App Router (GitHub Discussions)
Boa migração!
Processo completo de migração do Pages Router para o App Router no Next.js
Etapas completas da avaliação à entrada em produção, incluindo a escolha entre duas estratégias e soluções para problemas comuns.
⏱️ Estimated time: 80 hr
- 1
Step 1: Avalie se vale a pena migrar o projeto
Critérios de decisão:
• Necessidade de layouts aninhados: você precisa de layouts em vários níveis sem renderizá-los novamente ao trocar de página
• Espaço para otimização: o carregamento inicial leva mais de 3 segundos e pode ser melhorado
• Manutenção no longo prazo: o projeto será mantido por mais de 3 anos, então migrar cedo permite aproveitar os benefícios antes
Cenários em que a migração não é recomendada:
• O projeto será desativado em breve
• Site estático pequeno, com até 5 páginas
• A equipe não domina React 18
• O projeto depende de muitas bibliotecas de terceiros antigas - 2
Step 2: Escolha a estratégia de migração
Escolha conforme o tamanho do projeto:
Projeto pequeno, com menos de 10 páginas independentes → migração gradual:
• Migre uma página de cada vez
• Aceite o estado de loading durante a alternância
• Indicado para blogs e outros projetos com pouco acoplamento entre páginas
Projeto grande, com mais de 10 páginas e alta exigência de experiência do usuário → opção sem indisponibilidade:
• Reconstrua todas as páginas no diretório /app/new
• Use rewrites e parâmetros de consulta para controlar a versão exibida
• Coloque a nova versão em produção somente depois dos testes internos - 3
Step 3: Migre getServerSideProps
Etapas:
1. Transforme o componente da página em uma função async
2. Busque os dados diretamente no componente com fetch
3. Defina a opção de cache correta:
• getServerSideProps → cache: 'no-store'
• getStaticProps → cache: 'force-cache'
• getStaticProps + revalidate → next: { revalidate: 60 }
4. Separe a parte interativa do cliente em um Client Component:
• Busque os dados no servidor e repasse-os ao Client Component
• O Client Component cuida de useState, useEffect e de outras lógicas interativas - 4
Step 4: Ajuste rotas e navegação
Atualize o código relacionado ao roteamento:
• next/router → next/navigation
• useRouter().pathname → usePathname()
• useRouter().query → useSearchParams()
• Use o componente Link no lugar de router.push()
Atenção: o comportamento de useRouter em next/navigation é diferente daquele do Pages Router; prefira usar diretamente o componente Link. - 5
Step 5: Configure o sistema de layouts
Use layouts aninhados para evitar cintilação ao trocar de página:
• Crie app/layout.tsx como layout raiz, com Header e Footer
• Crie sublayouts para áreas funcionais, como app/dashboard/layout.tsx
• Em cada layout, adicione somente os elementos de UI específicos daquele nível
• O sublayout herda automaticamente o layout pai e não é renderizado novamente durante a navegação - 6
Step 6: Trate erros e páginas 404
Tratamento de erros:
• Crie error.tsx para capturar erros e exibir uma mensagem amigável
• Marque o componente de erro com 'use client'
Tratamento de 404:
• Exclua /pages/404.js
• Crie app/not-found.tsx
• Use a função notFound() em page.tsx para disparar o erro 404 - 7
Step 7: Teste e otimize
Pontos a testar:
• Verifique se todas as rotas funcionam
• Confirme se os dados são obtidos corretamente
• Teste as interações do cliente
• Confirme que não há cintilação ao trocar de layout
Otimização de desempenho:
• Reduza marcações 'use client' desnecessárias
• Use uma estratégia de cache adequada
• Monitore métricas como FCP, TTI e CLS
• Compare os dados de desempenho antes e depois da migração
FAQ
Qual é a diferença entre a migração gradual e a migração sem indisponibilidade?
Na migração sem indisponibilidade, todas as páginas são reconstruídas no diretório /app/new e um parâmetro de consulta controla a versão exibida. A nova versão só entra oficialmente em produção depois dos testes. É indicada para projetos grandes com alta exigência de experiência do usuário.
Como buscar dados depois de migrar getServerSideProps?
Defina a opção de cache correta:
• getServerSideProps corresponde a cache: 'no-store'
• getStaticProps corresponde a cache: 'force-cache'
Se a página tiver interações no cliente, separe-a em um Server Component, que busca os dados, e um Client Component, que cuida da interação.
Por que a página pisca durante a navegação depois da migração?
Como usar useRouter no App Router?
O que fazer quando uma biblioteca de terceiros fica incompatível após a migração?
Entre as bibliotecas frequentemente incompatíveis estão:
• Framer Motion
• Bibliotecas de gráficos, como ECharts e Chart.js
• Bibliotecas de carrossel
Antes de migrar, consulte as issues da biblioteca no GitHub para confirmar a compatibilidade.
Como resolver a lentidão do servidor de desenvolvimento?
Soluções temporárias:
• Reinicie o dev server periodicamente, de preferência a cada 15 minutos
• Use next dev --turbo para ativar o Turbopack
• Reduza Server Components desnecessários
Dizem que o Next.js 15 melhorou esse problema.
Quanto tempo leva a migração?
• Um projeto pequeno, com menos de 10 páginas, pode levar de 3 a 5 dias
• Um projeto grande pode levar de 2 a 3 semanas
Comece com 1 ou 2 páginas piloto. Depois de validar o processo, avance para o restante. Nossa equipe levou 3 dias na primeira página e apenas 5 dias nas 10 páginas seguintes.
20 min de leitura · Publicado em: 18 dez 2025 · Atualizado em: 4 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Como criar um blog com Next.js 15 em um fim de semana
Crie um blog full stack com Next.js 15, Server Actions, Prisma e PostgreSQL, da configuração à implantação na Vercel, com otimizações de SSG e SSR.
Parte 2 de 26
Próximo
Rotas avançadas no Next.js: guia completo de grupos, layouts aninhados, rotas paralelas e interceptação
Aprenda em profundidade quatro recursos avançados de roteamento do Next.js: grupos de rotas para organizar diretórios, layouts aninhados para reutilização flexível, rotas paralelas para exibir várias páginas e rotas interceptadas para criar modais elegantes. Inclui exemplos completos e cuidados práticos para resolver a desorganização das rotas e os conflitos de colaboração em projetos maiores.
Parte 4 de 26



Comentários
Entre com GitHub para comentar