comparaciones·14 min de lectura

App Router vs Pages Router en Next.js: Cual Usar y Por Que

Comparacion detallada entre App Router y Pages Router en Next.js. Routing, data fetching, layouts, SEO, rendimiento y migracion con ejemplos de codigo.

App Router vs Pages Router en Next.js: Cual Usar

La decision entre App Router vs Pages Router en NextJS es probablemente la primera que tomas al crear un proyecto nuevo, y la mas consecuente para toda la arquitectura de tu aplicacion. No es solo una preferencia de sintaxis -- cada router implica un modelo de datos, rendering y composicion fundamentalmente distinto.

Next.js mantiene ambos routers funcionales, pero la direccion del framework es clara: App Router es el presente y el futuro. Este articulo compara ambos con codigo real para que tomes una decision informada.

Pages Router: como funciona

Pages Router fue el sistema de routing original de NextJS. Lleva activo desde las primeras versiones del framework y durante anos fue la unica opcion. Su modelo es simple y directo.

Estructura de archivos

Cada archivo dentro de la carpeta pages/ se convierte automaticamente en una ruta:

Estructura de archivos

pages/ index.tsx --> / about.tsx --> /about contact.tsx --> /contact blog/ index.tsx --> /blog [slug].tsx --> /blog/cualquier-slug api/ users.ts --> /api/users posts/ [id].ts --> /api/posts/123 _app.tsx --> Layout global _document.tsx --> HTML base 404.tsx --> Pagina de error 404

El mapeo es 1:1 entre archivo y ruta. pages/about.tsx siempre sera /about. No hay ambiguedad.

Data fetching en Pages Router

Pages Router usa funciones especiales que se exportan junto al componente de la pagina. Cada una tiene un proposito diferente:

getServerSideProps (SSR)

Se ejecuta en el servidor en cada request. Util cuando los datos cambian frecuentemente:

tsx
// pages/dashboard.tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'
 
interface DashboardProps {
  usuarios: Array<{ id: string; nombre: string; email: string }>
  totalVentas: number
}
 
export const getServerSideProps: GetServerSideProps<DashboardProps> = async (context) => {
  const { req, res, query, params } = context
 
  // Acceso a cookies, headers, query params
  const token = req.cookies.session_token
 
  if (!token) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    }
  }
 
  const [usuariosRes, ventasRes] = await Promise.all([
    fetch('https://api.ejemplo.com/usuarios', {
      headers: { Authorization: `Bearer ${token}` },
    }),
    fetch('https://api.ejemplo.com/ventas/total', {
      headers: { Authorization: `Bearer ${token}` },
    }),
  ])
 
  const usuarios = await usuariosRes.json()
  const { total } = await ventasRes.json()
 
  return {
    props: {
      usuarios,
      totalVentas: total,
    },
  }
}
 
export default function Dashboard({
  usuarios,
  totalVentas,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <main>
      <h1>Dashboard</h1>
      <p>Total ventas: ${totalVentas}</p>
      <ul>
        {usuarios.map((u) => (
          <li key={u.id}>
            {u.nombre} -- {u.email}
          </li>
        ))}
      </ul>
    </main>
  )
}

La funcion getServerSideProps recibe un context con la request, response, parametros y query. Retorna un objeto con props que llegan directamente al componente.

getStaticProps (SSG)

Se ejecuta en build time. Genera HTML estatico que se sirve desde un CDN:

tsx
// pages/blog/[slug].tsx
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
 
interface Post {
  slug: string
  titulo: string
  contenido: string
  fecha: string
}
 
export const getStaticPaths: GetStaticPaths = async () => {
  const res = await fetch('https://api.ejemplo.com/posts')
  const posts: Post[] = await res.json()
 
  const paths = posts.map((post) => ({
    params: { slug: post.slug },
  }))
 
  return {
    paths,
    fallback: 'blocking', // Genera paginas nuevas bajo demanda
  }
}
 
export const getStaticProps: GetStaticProps<{ post: Post }> = async ({ params }) => {
  const res = await fetch(`https://api.ejemplo.com/posts/${params?.slug}`)
 
  if (!res.ok) {
    return { notFound: true }
  }
 
  const post: Post = await res.json()
 
  return {
    props: { post },
    revalidate: 3600, // ISR: regenera cada hora
  }
}
 
export default function BlogPost({
  post,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    <article>
      <h1>{post.titulo}</h1>
      <time>{post.fecha}</time>
      <div dangerouslySetInnerHTML={{ __html: post.contenido }} />
    </article>
  )
}

Con getStaticPaths le dices a NextJS que rutas generar en build time. getStaticProps obtiene los datos para cada una. La opcion revalidate habilita ISR (Incremental Static Regeneration) para actualizar el contenido sin hacer rebuild completo.

getStaticProps vs getServerSideProps

AspectogetStaticPropsgetServerSideProps
Cuando se ejecutaBuild time (+ ISR)Cada request
RendimientoRapido (cache CDN)Mas lento (servidor)
DatosPueden estar desactualizadosSiempre frescos
Uso idealBlog, docs, marketingDashboard, perfil, checkout
Fallbackblocking, true, falseNo aplica
ISRSi (revalidate)No aplica

Layout global en Pages Router

Pages Router tiene un sistema de layouts limitado. Usas _app.tsx para un layout global y componentes wrapper manuales para layouts por seccion:

tsx
// pages/_app.tsx
import type { AppProps } from 'next/app'
import { Layout } from '@/components/Layout'
 
export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}
tsx
// components/Layout.tsx
import { Header } from './Header'
import { Footer } from './Footer'
 
export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Header />
      <main>{children}</main>
      <Footer />
    </>
  )
}

El problema es que _app.tsx envuelve todas las paginas. Si necesitas un layout diferente para /dashboard y /blog, tienes que hacerlo manualmente con logica condicional o con un patron getLayout:

tsx
// pages/_app.tsx
import type { AppProps } from 'next/app'
import type { NextPage } from 'next'
import type { ReactElement, ReactNode } from 'react'
 
type NextPageWithLayout = NextPage & {
  getLayout?: (page: ReactElement) => ReactNode
}
 
type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}
 
export default function App({ Component, pageProps }: AppPropsWithLayout) {
  const getLayout = Component.getLayout ?? ((page) => page)
  return getLayout(<Component {...pageProps} />)
}
tsx
// pages/dashboard.tsx
import { DashboardLayout } from '@/components/DashboardLayout'
import type { ReactElement } from 'react'
 
export default function DashboardPage() {
  return <div>Contenido del dashboard</div>
}
 
DashboardPage.getLayout = function getLayout(page: ReactElement) {
  return <DashboardLayout>{page}</DashboardLayout>
}

Funciona, pero es un workaround. Los layouts no se preservan entre navegaciones y se re-renderizan completamente cada vez que cambias de pagina.

App Router: como funciona

App Router fue introducido en NextJS 13 y se volvio estable en NextJS 13.4. Usa la carpeta app/ en lugar de pages/ y trae un modelo completamente nuevo basado en Server Components.

Estructura de archivos

App Router usa convenciones de archivos especiales dentro de cada carpeta de ruta:

Estructura de archivos

app/ layout.tsx --> Layout raiz (envuelve todo) page.tsx --> / loading.tsx --> UI de carga para / error.tsx --> UI de error para / not-found.tsx --> Pagina 404 personalizada about/ page.tsx --> /about blog/ layout.tsx --> Layout para /blog y subrutas page.tsx --> /blog [slug]/ page.tsx --> /blog/cualquier-slug loading.tsx --> UI de carga para posts dashboard/ layout.tsx --> Layout del dashboard (persistente) page.tsx --> /dashboard settings/ page.tsx --> /dashboard/settings analytics/ page.tsx --> /dashboard/analytics api/ users/ route.ts --> /api/users (GET, POST, etc.) posts/ [id]/ route.ts --> /api/posts/123

La diferencia fundamental: en App Router, cada carpeta puede tener archivos con propositos especificos (page.tsx, layout.tsx, loading.tsx, error.tsx, template.tsx). No es solo una carpeta = una ruta, sino una carpeta = un segmento de ruta con toda su configuracion.

Archivos especiales de App Router

ArchivoProposito
page.tsxUI de la ruta. Hace que la carpeta sea accesible como URL
layout.tsxLayout compartido. Se preserva entre navegaciones
loading.tsxUI de carga automatica (usa Suspense internamente)
error.tsxBoundary de error (usa Error Boundary internamente)
template.tsxSimilar a layout pero se re-monta en cada navegacion
not-found.tsxUI para cuando la pagina no existe
route.tsAPI Route handler (reemplaza pages/api)

Data fetching en App Router

App Router elimina getServerSideProps, getStaticProps y getStaticPaths. En su lugar, haces fetch directamente en los Server Components:

Equivalente a getServerSideProps

tsx
// app/dashboard/page.tsx
// Server Component por defecto, se ejecuta en cada request
 
interface Usuario {
  id: string
  nombre: string
  email: string
}
 
async function obtenerUsuarios(token: string): Promise<Usuario[]> {
  const res = await fetch('https://api.ejemplo.com/usuarios', {
    headers: { Authorization: `Bearer ${token}` },
    cache: 'no-store', // Equivalente a getServerSideProps: datos frescos siempre
  })
 
  if (!res.ok) throw new Error('Error al obtener usuarios')
  return res.json()
}
 
export default async function DashboardPage() {
  const { cookies } = await import('next/headers')
  const cookieStore = await cookies()
  const token = cookieStore.get('session_token')?.value
 
  if (!token) {
    const { redirect } = await import('next/navigation')
    redirect('/login')
  }
 
  const usuarios = await obtenerUsuarios(token)
 
  return (
    <main>
      <h1>Dashboard</h1>
      <ul>
        {usuarios.map((u) => (
          <li key={u.id}>
            {u.nombre} -- {u.email}
          </li>
        ))}
      </ul>
    </main>
  )
}

La clave es cache: 'no-store' en el fetch. Esto le dice a NextJS que no cachee la respuesta y la obtenga fresca en cada request, similar a como funciona getServerSideProps.

Equivalente a getStaticProps + ISR

tsx
// app/blog/[slug]/page.tsx
 
interface Post {
  slug: string
  titulo: string
  contenido: string
  fecha: string
}
 
async function obtenerPost(slug: string): Promise<Post | null> {
  const res = await fetch(`https://api.ejemplo.com/posts/${slug}`, {
    next: { revalidate: 3600 }, // ISR: revalida cada hora
  })
 
  if (!res.ok) return null
  return res.json()
}
 
// Equivalente a getStaticPaths
export async function generateStaticParams() {
  const res = await fetch('https://api.ejemplo.com/posts')
  const posts: Post[] = await res.json()
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}
 
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await obtenerPost(slug)
 
  if (!post) {
    const { notFound } = await import('next/navigation')
    notFound()
  }
 
  return (
    <article>
      <h1>{post.titulo}</h1>
      <time>{post.fecha}</time>
      <div dangerouslySetInnerHTML={{ __html: post.contenido }} />
    </article>
  )
}

generateStaticParams reemplaza a getStaticPaths. La opcion next: { revalidate: 3600 } en fetch reemplaza a revalidate de getStaticProps.

Layouts en App Router

Los layouts son una de las mejores mejoras de App Router. Son persistentes: no se re-renderizan cuando navegas entre rutas del mismo segmento.

tsx
// app/layout.tsx (Layout raiz - aplica a TODAS las paginas)
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: {
    template: '%s | Mi Sitio',
    default: 'Mi Sitio',
  },
  description: 'Descripcion del sitio',
}
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="es">
      <body>
        <header>
          <nav>
            <a href="/">Inicio</a>
            <a href="/blog">Blog</a>
            <a href="/dashboard">Dashboard</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>
          <p>Footer del sitio</p>
        </footer>
      </body>
    </html>
  )
}
tsx
// app/dashboard/layout.tsx (Layout solo para /dashboard/*)
import { DashboardNav } from '@/components/DashboardNav'
 
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex">
      <DashboardNav />
      <div className="flex-1 p-6">{children}</div>
    </div>
  )
}

Cuando navegas de /dashboard a /dashboard/settings, el DashboardLayout no se re-renderiza. Solo cambia el children. Esto es imposible de lograr en Pages Router sin workarounds.

Metadata y SEO en App Router

App Router tiene un sistema nativo de metadata que genera automaticamente las tags de SEO:

tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
 
// Metadata dinamica basada en los parametros
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await obtenerPost(slug)
 
  if (!post) {
    return { title: 'Post no encontrado' }
  }
 
  return {
    title: post.titulo,
    description: post.resumen,
    openGraph: {
      title: post.titulo,
      description: post.resumen,
      type: 'article',
      publishedTime: post.fecha,
      images: [{ url: post.imagen }],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.titulo,
      description: post.resumen,
    },
  }
}
 
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await obtenerPost(slug)
  // ...
}

Para una estrategia de SEO completa, un sitemap automatico complementa la metadata individual de cada pagina.

En Pages Router, la metadata requiere el componente Head de next/head en cada pagina:

tsx
// pages/blog/[slug].tsx (Pages Router)
import Head from 'next/head'
 
export default function BlogPost({ post }) {
  return (
    <>
      <Head>
        <title>{post.titulo}</title>
        <meta name="description" content={post.resumen} />
        <meta property="og:title" content={post.titulo} />
        <meta property="og:description" content={post.resumen} />
      </Head>
      <article>
        <h1>{post.titulo}</h1>
      </article>
    </>
  )
}

La diferencia: con App Router, la metadata se maneja de forma declarativa y tipada. Con Pages Router, manejas HTML tags manualmente.

Tabla comparativa detallada

AspectoPages RouterApp Router
Carpetapages/app/
Modelo de renderingTodo es Client ComponentServer Components por defecto
Data fetchinggetServerSideProps, getStaticPropsasync components + fetch
Rutas estaticasgetStaticPathsgenerateStaticParams
Layouts_app.tsx + getLayout manuallayout.tsx anidados y persistentes
Loading UIManual con estadosloading.tsx automatico
Error handling_error.tsx globalerror.tsx por segmento
Metadata/SEOnext/head manualgenerateMetadata tipado
API Routespages/api/*.tsapp/api/*/route.ts
Router hookuseRouter de next/routeruseRouter de next/navigation
Streaming SSRNo nativoSi, con Suspense
Server ActionsNo disponibleSi ("use server")
Parallel RoutesNo disponibleSi (@folder)
Intercepting RoutesNo disponibleSi ((..)folder)
Route GroupsNo disponibleSi ((folder))
MiddlewareSiSi (mismo sistema)
Sitemap nativoNoSi (sitemap.ts)
Bundle sizeTodo va al clienteSolo Client Components
StatusMantenimiento (sin features nuevas)Desarrollo activo

Routing avanzado exclusivo de App Router

App Router introduce conceptos de routing que no tienen equivalente en Pages Router.

Route Groups

Permiten organizar rutas sin afectar la URL:

Estructura de archivos

app/ (marketing)/ page.tsx --> / about/ page.tsx --> /about pricing/ page.tsx --> /pricing layout.tsx --> Layout para marketing (dashboard)/ dashboard/ page.tsx --> /dashboard settings/ page.tsx --> /dashboard/settings layout.tsx --> Layout para dashboard

Los parentesis en (marketing) y (dashboard) no aparecen en la URL. Sirven solo para agrupar rutas que comparten el mismo layout.

Parallel Routes

Permiten renderizar multiples paginas en la misma vista simultaneamente:

tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  equipo,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  equipo: React.ReactNode
}) {
  return (
    <div>
      {children}
      <div className="grid grid-cols-2 gap-4">
        {analytics}
        {equipo}
      </div>
    </div>
  )
}
Estructura de archivos

app/ dashboard/ page.tsx @analytics/ page.tsx loading.tsx @equipo/ page.tsx loading.tsx layout.tsx

Cada slot (@analytics, @equipo) se carga de forma independiente. Si analytics tarda 3 segundos y equipo tarda 1, el usuario ve equipo mientras analytics muestra su loading.tsx.

Intercepting Routes

Permiten abrir una ruta en un modal sin navegar completamente:

plaintext
app/
  feed/
    page.tsx              --> /feed (lista de fotos)
    (..)foto/[id]/
      page.tsx            --> Intercepta /foto/123 y muestra en modal
  foto/
    [id]/
      page.tsx            --> /foto/123 (pagina completa, acceso directo)

Cuando haces click en una foto desde el feed, se abre en un modal (ruta interceptada). Si compartes el link /foto/123 directamente, se abre como pagina completa. Instagram funciona exactamente asi.

Migracion de Pages Router a App Router

Si tienes un proyecto existente con Pages Router, la migracion es gradual. No necesitas convertir todo de una vez.

Estrategia recomendada

1

Crea la carpeta app/ junto a pages/

Ambas pueden coexistir. Las rutas en app/ tienen prioridad si hay conflicto.

2

Mueve el layout global primero

Convierte _app.tsx y _document.tsx en app/layout.tsx:

tsx
// app/layout.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'Mi Sitio',
  description: 'Descripcion',
}
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="es">
      <body>{children}</body>
    </html>
  )
}
3

Migra una pagina simple para probar

Elige una pagina sin data fetching complejo (como /about) y muevela:

tsx
// Antes: pages/about.tsx
export default function About() {
  return <h1>Sobre nosotros</h1>
}
 
// Despues: app/about/page.tsx
export default function AboutPage() {
  return <h1>Sobre nosotros</h1>
}
4

Convierte el data fetching progresivamente

Reemplaza getServerSideProps con Server Components:

tsx
// Antes: pages/products.tsx
export async function getServerSideProps() {
  const res = await fetch('https://api.ejemplo.com/products')
  const products = await res.json()
  return { props: { products } }
}
 
export default function Products({ products }) {
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  )
}
 
// Despues: app/products/page.tsx
export default async function ProductsPage() {
  const res = await fetch('https://api.ejemplo.com/products', {
    cache: 'no-store',
  })
  const products = await res.json()
 
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  )
}
5

Actualiza las importaciones del router

tsx
// Antes (Pages Router)
import { useRouter } from 'next/router'
 
const router = useRouter()
router.push('/destino')
router.query.slug // parametros de la URL
 
// Despues (App Router)
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
 
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
 
router.push('/destino')
// Los params llegan como prop en page.tsx
6

Elimina la carpeta pages/ cuando todo este migrado

Una vez que todas las rutas estan en app/, puedes eliminar pages/ y sus archivos especiales (_app.tsx, _document.tsx, _error.tsx).

Cambios especificos en la migracion

tsx
// Pages Router
import { useRouter } from 'next/router'
import Link from 'next/link'
 
function Componente() {
  const router = useRouter()
 
  // Ruta actual
  const ruta = router.pathname      // '/blog/[slug]'
  const rutaReal = router.asPath    // '/blog/mi-post'
 
  // Query params
  const { slug } = router.query     // { slug: 'mi-post' }
 
  // Navegar
  router.push('/destino')
  router.replace('/destino')
  router.back()
 
  return <Link href="/blog">Blog</Link>
}
tsx
// App Router
'use client'
 
import { useRouter, usePathname, useSearchParams, useParams } from 'next/navigation'
import Link from 'next/link'
 
function Componente() {
  const router = useRouter()
  const pathname = usePathname()         // '/blog/mi-post'
  const searchParams = useSearchParams() // URLSearchParams
  const params = useParams()             // { slug: 'mi-post' }
 
  // Navegar
  router.push('/destino')
  router.replace('/destino')
  router.back()
  router.refresh() // Nuevo: re-ejecuta Server Components
 
  return <Link href="/blog">Blog</Link>
}

La diferencia principal: en App Router, useRouter viene de next/navigation (no next/router), y las funcionalidades estan separadas en hooks individuales (usePathname, useSearchParams, useParams).

⚠️
Importacion correcta

Si importas useRouter de next/router dentro de app/, vas a obtener un error. Asegurate de importar siempre de next/navigation en componentes dentro de la carpeta app/.

API Routes

tsx
// Pages Router: pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'
 
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'GET') {
    res.status(200).json({ users: [] })
  } else if (req.method === 'POST') {
    const body = req.body
    res.status(201).json({ created: true })
  } else {
    res.status(405).end()
  }
}
tsx
// App Router: app/api/users/route.ts
import { NextResponse } from 'next/server'
 
export async function GET() {
  return NextResponse.json({ users: [] })
}
 
export async function POST(request: Request) {
  const body = await request.json()
  return NextResponse.json({ created: true }, { status: 201 })
}

En App Router, cada metodo HTTP es una funcion exportada separada. No necesitas el switch/if sobre req.method. Ademas, usa las APIs estandar de Request y Response del Web Platform.

Error handling

tsx
// Pages Router: pages/_error.tsx (global, unico)
function ErrorPage({ statusCode }) {
  return <p>Error: {statusCode}</p>
}
 
ErrorPage.getInitialProps = ({ res, err }) => {
  const statusCode = res ? res.statusCode : err ? err.statusCode : 404
  return { statusCode }
}
 
export default ErrorPage
tsx
// App Router: app/blog/error.tsx (por segmento, granular)
'use client' // error.tsx debe ser Client Component
 
export default function BlogError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Error al cargar el blog</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Intentar de nuevo</button>
    </div>
  )
}

App Router permite error boundaries por segmento de ruta. Si falla la carga del blog, solo la seccion del blog muestra el error -- el header, footer y sidebar siguen funcionando.

Cuando usar cada uno

Usa App Router cuando:

  • Proyecto nuevo: No hay razon para empezar con Pages Router en 2026
  • SEO es prioridad: Metadata tipada, sitemaps nativos, Server Components reducen JS
  • Layouts complejos: Dashboards, paneles admin, sitios con sidebars persistentes
  • Rendimiento importa: Server Components, streaming, bundle reducido
  • Necesitas funcionalidades nuevas: Server Actions, Parallel Routes, Intercepting Routes

Usa Pages Router cuando:

  • Proyecto existente estable: Si funciona y no necesita features nuevas, no lo toques
  • Equipo no tiene tiempo para migrar: La migracion tiene costo y riesgo
  • Dependencias incompatibles: Algunas librerias antiguas no funcionan con Server Components
  • Prototipo rapido: Si la velocidad de desarrollo importa mas que la arquitectura (aunque hoy App Router ya es igual de rapido para prototipar)
ℹ️
Recomendacion directa

Si vas a crear un proyecto nuevo, usa App Router. El ecosistema, las herramientas, la documentacion, y las mejoras de rendimiento estan todas enfocadas ahi. Pages Router es para mantener lo que ya existe.

Rendimiento comparado

Bundle size

Con Pages Router, todo el codigo del componente de la pagina va al navegador, incluyendo logica de renderizado, formateo de datos y dependencias.

Con App Router, solo los Client Components envian JavaScript al navegador. Un blog post que se renderiza con marked (500KB) en un Server Component envia 0KB de JavaScript adicional al cliente.

Time to First Byte (TTFB)

Ambos routers pueden lograr tiempos similares con SSR. La diferencia esta en el streaming: App Router puede empezar a enviar HTML antes de que todo este listo, mientras Pages Router espera a que getServerSideProps termine completamente.

tsx
// App Router con streaming
// El usuario ve el header inmediatamente mientras las secciones cargan
import { Suspense } from 'react'
 
export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<Skeleton />}>
        <GraficaVentas />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <TablaUsuarios />
      </Suspense>
    </div>
  )
}

Con Pages Router, el usuario no ve nada hasta que todas las queries del getServerSideProps terminan. Con App Router y Suspense, el usuario ve contenido progresivamente.

En Pages Router, cambiar de pagina descarga el JavaScript de la nueva pagina, ejecuta getServerSideProps (si aplica), y renderiza el componente. El layout se destruye y se recrea.

En App Router, los layouts se preservan. Solo se actualiza el segmento que cambia. Esto significa menos JavaScript descargado, menos trabajo del navegador, y transiciones mas fluidas.

Estructura recomendada para App Router

Para un proyecto nuevo con App Router, esta estructura cubre la mayoria de los casos:

Estructura de archivos

app/ (marketing)/ layout.tsx page.tsx --> / about/ page.tsx --> /about blog/ page.tsx --> /blog [slug]/ page.tsx --> /blog/post-slug (app)/ layout.tsx dashboard/ page.tsx --> /dashboard loading.tsx error.tsx settings/ page.tsx --> /dashboard/settings analytics/ page.tsx --> /dashboard/analytics api/ users/ route.ts webhooks/ route.ts layout.tsx --> Root layout not-found.tsx sitemap.ts robots.ts

Los Route Groups (marketing) y (app) separan las secciones publicas del area autenticada, cada una con su propio layout, sin afectar las URLs.

Preguntas frecuentes

getInitialProps sigue funcionando en App Router?

No. getInitialProps solo funciona en Pages Router y se considera deprecada incluso ahi. En App Router, usa Server Components con async/await para obtener datos.

Puedo usar middleware con ambos routers?

Si. El middleware (middleware.ts en la raiz del proyecto) funciona igual con ambos routers. Se ejecuta antes del rendering, independientemente de si la ruta esta en pages/ o app/.

El rendimiento de Pages Router es peor?

No necesariamente. Pages Router con getStaticProps y un CDN puede ser muy rapido. La ventaja de App Router esta en Server Components (menos JS al cliente), streaming (contenido progresivo), y layouts persistentes (menos re-renders en navegacion). Para paginas completamente estaticas, la diferencia es minima.

next/image funciona igual en ambos routers. next/link tambien, pero en App Router ya no necesitas la prop legacyBehavior y el componente <a> interno se agrega automaticamente. En App Router, <Link href="/blog">Blog</Link> es suficiente -- no necesitas un <a> hijo.

Puedo usar ISR (Incremental Static Regeneration) en App Router?

Si. ISR en App Router se configura con la opcion revalidate en fetch o con export const revalidate = 3600 a nivel de segmento de ruta. Tambien puedes usar revalidatePath() y revalidateTag() para invalidacion bajo demanda, que es mas flexible que el ISR basado en tiempo de Pages Router.


Recursos adicionales

#nextjs#app-router#pages-router#routing#migracion

Preguntas frecuentes

Deberia usar App Router o Pages Router para un proyecto nuevo en Next.js?

Para proyectos nuevos, usa App Router. Es el estandar recomendado por el equipo de NextJS, soporta Server Components, layouts anidados, streaming SSR y tiene mejor rendimiento por defecto. Pages Router sigue funcionando pero no recibe nuevas funcionalidades.

Puedo usar App Router y Pages Router en el mismo proyecto?

Si. NextJS permite usar ambos routers simultaneamente. Las rutas en app/ usan App Router y las rutas en pages/ usan Pages Router. Esto facilita la migracion gradual, pero debes tener cuidado de no crear la misma ruta en ambos directorios porque causara un conflicto.

Que reemplaza a getServerSideProps en App Router?

En App Router no necesitas funciones especiales para obtener datos. Los Server Components pueden hacer fetch directamente con async/await dentro del componente. Para revalidacion, usas la opcion next: { revalidate: seconds } en fetch o la funcion revalidatePath/revalidateTag para invalidacion bajo demanda.

Es dificil migrar de Pages Router a App Router?

Depende del tamano del proyecto. NextJS permite migracion gradual -- puedes mover una ruta a la vez. Los cambios principales son: pasar de pages/ a app/, reemplazar getServerSideProps/getStaticProps con Server Components, adoptar el nuevo sistema de layouts, y cambiar de next/router a next/navigation.

Pages Router va a desaparecer de NextJS?

No en el corto plazo. El equipo de NextJS ha confirmado que Pages Router seguira siendo soportado por varias versiones mas. Sin embargo, todas las nuevas funcionalidades se desarrollan exclusivamente para App Router, por lo que Pages Router no recibira mejoras nuevas.