Alternar tema

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

Easton editorial illustration: island architecture model

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:

  1. Atualize o Next.js para a versão 14 — primeiro, atualize apenas o framework, sem mexer em /pages
  2. Migre os hooks de roteamento — troque next/router por next/navigation e garanta que o código seja compatível com os dois roteadores
  3. Crie o diretório /app/new — reconstrua ali a estrutura das páginas
  4. Reutilize os componentes existentes — importe diretamente os componentes React de /pages, sem reescrevê-los
  5. Configure rewrites — adicione a configuração acima e use ?new=true para alternar a versão
  6. Faça testes internos e uma liberação gradual — permita que a equipe use a nova versão e corrija rapidamente os problemas encontrados
  7. Coloque em produção — mova /app/new para /app e 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:

  • getServerSidePropscache: 'no-store'
  • getStaticPropscache: 'force-cache', o comportamento padrão
  • getStaticProps + revalidatenext: { 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 RouterApp Router
useRouter().push(url)useRouter().push(url) (existe, mas não é recomendado)
useRouter().pathnameusePathname()
useRouter().queryuseSearchParams()
useRouter().asPathusePathname() + 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 --turbo para 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:

  1. Envolva os componentes que usam essas bibliotecas com 'use client'
  2. Verifique se existe uma versão mais recente da biblioteca com suporte a React 18; algumas já foram adaptadas
  3. 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:

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. 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. 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. 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. 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. 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. 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. 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 gradual, você migra uma página de cada vez. Ela é indicada para projetos pequenos, mas a alternância entre /pages e /app apresenta um estado de loading.

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?
Transforme o componente da página em uma função async e busque os dados diretamente dentro dele com fetch.

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?
Geralmente isso acontece porque o sistema de layouts não foi usado corretamente. Crie app/layout.tsx como layout raiz e sublayouts para cada área funcional. Assim, somente a área de conteúdo é atualizada durante a navegação; a barra de navegação e a barra lateral não são renderizadas novamente, evitando a cintilação.
Como usar useRouter no App Router?
O App Router usa next/navigation no lugar de next/router. Substitua useRouter().pathname por usePathname() e useRouter().query por useSearchParams(). Para navegar, prefira o componente Link em vez de router.push().
O que fazer quando uma biblioteca de terceiros fica incompatível após a migração?
Marque com 'use client' os componentes que usam essas bibliotecas.

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?
Esse é um problema conhecido do Next.js 14.

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?
Depende do tamanho do projeto:
• 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

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog