comparaciones·14 min de lectura

App Router vs Pages Router en Next.js: Cual Usar y por qué

Comparación detallada entre App Router y Pages Router en Next.js. Routing, data fetching, layouts, SEO, rendimiento y migración con ejemplos de código.

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 más consecuente para toda la arquitectura de tu aplicación. No es solo una preferencia de sintaxis -- cada router implica un modelo de datos, rendering y composición fundamentalmente distinto.

Next.js mantiene ambos routers funcionales, pero la dirección del framework es clara: App Router es el presente y el futuro. Este artículo compara ambos con código real para que tomes una decision informada.

Pages Router: cómo funciona

Pages Router fue el sistema de routing original de NextJS. Lleva activo desde las primeras versiones del framework y durante años fue la única opción. Su modelo es simple y directo.

Estructura de archivos

Cada archivo dentro de la carpeta pages/ se convierte automáticamente 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 --> página de error 404

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

Data fetching en Pages Router

Pages Router usa funciones especiales que se exportan junto al componente de la página. Cada una tiene un propósito diferente:

getServerSideProps (SSR)

Se ejecuta en el servidor en cada request. útil 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 función getServerSideProps recibe un context con la request, response, parámetros y query. Retorna un objeto con props que llegan directamente al componente.

getStaticProps (SSG)

Se ejecuta en build time. Genera HTML estático que se sirve desde un CDN:

tsx
// pages/blog/[slug].tsx
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
 
interface Post {
  slug: string
  título: 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 páginas 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.título}</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 opción 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
Rendimientorápido (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 sección:

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 páginas. Si necesitas un layout diferente para /dashboard y /blog, tienes que hacerlo manualmente con lógica 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 página.

App Router: cómo 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 --> página 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 configuración.

Archivos especiales de App Router

Archivopropósito
page.tsxUI de la ruta. Hace que la carpeta sea accesible como URL
layout.tsxLayout compartido. Se preserva entre navegaciones
loading.tsxUI de carga automática (usa Suspense internamente)
error.tsxBoundary de error (usa Error Boundary internamente)
template.tsxSimilar a layout pero se re-monta en cada navegación
not-found.tsxUI para cuando la página 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 cómo funciona getServerSideProps.

Equivalente a getStaticProps + ISR

tsx
// app/blog/[slug]/page.tsx
 
interface Post {
  slug: string
  título: 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.título}</h1>
      <time>{post.fecha}</time>
      <div dangerouslySetInnerHTML={{ __html: post.contenido }} />
    </article>
  )
}

generateStaticParams reemplaza a getStaticPaths. La opción 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 páginas)
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: {
    template: '%s | Mi Sitio',
    default: 'Mi Sitio',
  },
  description: 'Descripción 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 automáticamente las tags de SEO:

tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
 
// Metadata dinámica basada en los parámetros
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.título,
    description: post.resumen,
    openGraph: {
      title: post.título,
      description: post.resumen,
      type: 'article',
      publishedTime: post.fecha,
      images: [{ url: post.imagen }],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.título,
      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 automático complementa la metadata individual de cada página.

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

tsx
// pages/blog/[slug].tsx (Pages Router)
import Head from 'next/head'
 
export default function BlogPost({ post }) {
  return (
    <>
      <Head>
        <title>{post.título}</title>
        <meta name="description" content={post.resumen} />
        <meta property="og:title" content={post.título} />
        <meta property="og:description" content={post.resumen} />
      </Head>
      <article>
        <h1>{post.título}</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 estáticasgetStaticPathsgenerateStaticParams
Layouts_app.tsx + getLayout manuallayout.tsx anidados y persistentes
Loading UIManual con estadosloading.tsx automático
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 múltiples páginas 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 (página 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 página completa. Instagram funciona exactamente así.

Migración de Pages Router a App Router

Si tienes un proyecto existente con Pages Router, la migración 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: 'Descripción',
}
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="es">
      <body>{children}</body>
    </html>
  )
}
3

Migra una página simple para probar

Elige una página sin data fetching complejo (como /about) y muevela:

tsx
// Antes: pages/about.tsx
export default function About() {
  return <h1>Sobre nosotros</h1>
}
 
// después: 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>
  )
}
 
// después: 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 // parámetros de la URL
 
// después (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 migración

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).

Importación correcta

Si importas useRouter de next/router dentro de app/, vas a obtener un error. Asegúrate 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 método HTTP es una función exportada separada. No necesitas el switch/if sobre req.method. además, usa las APIs estandar de Request y Response del Web Platform.

Error handling

tsx
// Pages Router: pages/_error.tsx (global, único)
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 sección del blog muestra el error -- el header, footer y sidebar siguen funcionando.

cuándo 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 migración tiene costo y riesgo
  • Dependencias incompatibles: Algunas librerías antiguas no funcionan con Server Components
  • Prototipo rápido: Si la velocidad de desarrollo importa más que la arquitectura (aunque hoy App Router ya es igual de rápido para prototipar)
Recomendación directa

Si vas a crear un proyecto nuevo, usa App Router. El ecosistema, las herramientas, la documentación, 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 código del componente de la página va al navegador, incluyendo lógica 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 página descarga el JavaScript de la nueva página, 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 más 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 rápido. La ventaja de App Router esta en Server Components (menos JS al cliente), streaming (contenido progresivo), y layouts persistentes (menos re-renders en navegación). Para páginas completamente estáticas, la diferencia es mínima.

next/image funciona igual en ambos routers. next/link también, pero en App Router ya no necesitas la prop legacyBehavior y el componente <a> interno se agrega automáticamente. 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 opción revalidate en fetch o con export const revalidate = 3600 a nivel de segmento de ruta. también puedes usar revalidatePath() y revalidateTag() para invalidación bajo demanda, qué es más flexible que el ISR basado en tiempo de Pages Router.


Recursos adicionales

#nextjs#app-router#pages-router#routing#migración

Preguntas frecuentes

¿Debería 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 migración gradual, pero debes tener cuidado de no crear la misma ruta en ambos directorios porque causara un conflicto.

¿Qué 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 revalidación, usas la opción next: { revalidate: seconds } en fetch o la función revalidatePath/revalidateTag para invalidación bajo demanda.

¿Es difícil migrar de Pages Router a App Router?

Depende del tamaño del proyecto. NextJS permite migración 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 más. Sin embargo, todas las nuevas funcionalidades se desarrollan exclusivamente para App Router, por lo que Pages Router no recibira mejoras nuevas.