El enrutamiento constituye la columna vertebral de una aplicación web, definiendo cómo se mapean las URLs al código, datos y árbol de componentes en tiempo de ejecución. En las SPAs tradicionales de cliente (basadas en react-router), la navegación depende de objetos centralizados de configuración o árboles declarativos de componentes. A escala empresarial, este enfoque introduce graves cuellos de botella:
React.lazy), donde cualquier descuido filtra dependencias pesadas en el bundle inicial.Next.js App Router redefine este paradigma mediante el enrutamiento basado en archivos (file-based routing). La estructura del sistema de archivos dentro de la carpeta app se compila directamente por el empaquetador Turbopack en el grafo de rutas de la aplicación. Esto elimina registros manuales, aísla páginas, automatiza el code splitting a nivel de segmento y garantiza un tamaño de bundle de cliente cero (Zero-Bundle Size) para los Server Components por defecto.
En aplicaciones React clásicas, el enrutamiento se configura manualmente mediante código (como cadenas declarativas de <Route path="..." element={<Component />} />). En Next.js App Router, el sistema de archivos es la configuración. Las carpetas definen rutas URL, mientras que los nombres de archivo reservados (page.tsx, layout.tsx, error.tsx) construyen la jerarquía de React sin requerir boilerplate de enrutamiento.
Cada página se compila en un bundle de JavaScript con todas sus librerías.
pages/dashboard.tsx → Client Bundle + getServerSidePropsComponentes de Servidor por defecto (RSC). Cero bytes de JS en el bundle del cliente.
app/dashboard/page.tsx → Zero-Bundle RSC + StreamingEn el modelo de App Router, la unidad estructural elemental es el segmento de ruta (Route Segment). Cada carpeta anidada dentro de app representa un segmento URL independiente.
Por ejemplo, la carpeta app/dashboard/settings corresponde con exactitud a la URL /dashboard/settings. Sin embargo, crear carpetas no expone automáticamente rutas públicas. Un segmento se convierte en un endpoint navegable solo cuando contiene un archivo hoja especial: page.tsx. Si page.tsx no existe, la carpeta permanece como una rama organizativa interna, utilizada exclusivamente para agrupar rutas o compartir layouts comunes.
Cada segmento está aislado y garantiza la preservación de estado (Preserved State): los layouts padre no se desmontan ni re-renderizan al cambiar de página hija, y las actualizaciones se transmiten como un flujo diferencial de React Server Components.
Haga clic en las rutas para observar el aislamiento de Preserved State en layouts
Cambie de ruta: ¡este texto no se reiniciará!
Navegación interna en el panel: ¡DashboardLayout NO se vuelve a montar! El texto escrito se conserva.
El ciclo de resolución de una ruta —desde la interacción del usuario hasta la reconciliación final en el DOM del navegador— sigue una secuencia estricta gestionada por Turbopack y el motor de streaming de React 19:
El navegador inicia una navegación suave (Soft Navigation). El enrutador de cliente envía una petición en segundo plano (Prefetch) con encabezados HTTP RSC: 1 y el árbol actual codificado en Next-Router-State-Tree.
El servidor compara la URL objetivo con el grafo de app/ y calcula el Ancestro Común Más Bajo (LCA) —el punto de bifurcación entre los segmentos activo y de destino.
El servidor ejecuta exclusivamente los componentes de servidor que han cambiado y transmite el payload diferencial en formato text/x-component (Flight Payload). Los layouts comunes no se vuelven a ejecutar.
El motor React Fiber realiza la reconciliación del subárbol, montando los nuevos nodos directamente en la ranura children del layout común más cercano. Los nodos del DOM padre y el foco de entrada se conservan.
El RSC Flight Payload recibido se almacena en la memoria del navegador en la Router Cache siguiendo las reglas staleTimes para navegación instantánea adelante/atrás.
¿Qué ocurre con el estado local (como el texto escrito en un input de búsqueda o un dropdown desplegado) dentro de app/dashboard/layout.tsx cuando un usuario navega de /dashboard/analytics a /dashboard/settings?
Next.js asigna responsabilidades sistémicas específicas a archivos con nombres reservados. Turbopack compila estos archivos en una jerarquía estricta de componentes de React.
page.tsx — El único punto de entrada que genera una URL pública. Sin este archivo, la carpeta no produce una ruta accesible.layout.tsx — La estructura base de un segmento. Debe recibir la prop { children: React.ReactNode }. app/layout.tsx (Root Layout) es obligatorio en todo el proyecto y es el único lugar donde se declaran las etiquetas <html> y <body>.loading.tsx — Límite declarativo de <Suspense>, que entrega inmediatamente un skeleton durante la carga en servidor.error.tsx — Capturador aislado de errores en cliente (<ErrorBoundary>). Debe incluir obligatoriamente la directiva 'use client'. Si una página hija falla, el layout padre y la navegación permanecen plenamente operativos.not-found.tsx — Interfaz 404 del segmento, activada programáticamente con notFound().(folder) — Route Group (Grupo de rutas): organiza funcionalidades o aísla layouts sin modificar la estructura URL. Por ejemplo, app/(marketing)/about/page.tsx responde en la ruta limpia /about._folder — Private Folder (Carpeta privada): queda excluida del sistema de rutas.app/
├── layout.tsx # Root Layout (<html> y <body>)
├── page.tsx # Página de inicio (/)
├── not-found.tsx # 404 global
├── global-error.tsx # Límite raíz de errores fatales
├── (marketing)/ # Route Group (no altera la URL)
│ ├── about/
│ │ └── page.tsx # Ruta: /about
│ └── pricing/
│ └── page.tsx # Ruta: /pricing
└── dashboard/
├── layout.tsx # Layout anidado con sidebar persistente
├── loading.tsx # Suspense fallback para dashboard
├── error.tsx # Client Error Boundary ('use client')
├── not-found.tsx # 404 de segmento
├── page.tsx # Ruta: /dashboard
├── _components/ # Carpeta privada (fuera de enrutamiento)
│ └── MetricCard.tsx
└── settings/
└── page.tsx # Ruta: /dashboard/settingsA partir de Next.js 15 y los estándares de React 19, los parámetros de ruta params y los parámetros de búsqueda searchParams se entregan a las páginas, layouts y funciones generateMetadata como Promesas asíncronas (Promise).
En el modelo síncrono (Next.js 13–14), el servidor debía retrasar la generación de la página hasta parsear completamente la petición HTTP entrante (incluyendo cabeceras y cookies). Esto bloqueaba artificialmente el tiempo hasta el primer byte (TTFB).
La transición a Promise es la base arquitectónica de Partial Prerendering (PPR):
await params está envuelta en un límite <Suspense> de React, la parte estática no espera. En cuanto el runtime resuelve la petición, el servidor envía los fragmentos dinámicos a través de la conexión HTTP abierta.import { Metadata } from 'next';
import { notFound } from 'next/navigation';
interface AnalyticsPageProps {
params: Promise<{ period: string }>;
searchParams: Promise<{ metric?: string }>;
}
// 1. Generación dinámica de metadatos
export async function generateMetadata(props: AnalyticsPageProps): Promise<Metadata> {
const { period } = await props.params;
return {
title: `Informe de periodo: ${period.toUpperCase()}`,
description: `Telemetría agregada para el periodo ${period}`,
};
}
// 2. Página de servidor con parámetros asíncronos
export default async function AnalyticsPage(props: AnalyticsPageProps) {
// Resolución asíncrona de parámetros en Next.js 15+
const params = await props.params;
const searchParams = await props.searchParams;
const validPeriods = ['day', 'week', 'month'];
if (!validPeriods.includes(params.period)) {
notFound();
}
const activeMetric = searchParams.metric ?? 'all';
return (
<main className="p-6 space-y-4">
<h1 className="text-2xl font-bold">Periodo: {params.period}</h1>
<p className="text-text-muted">Filtro métrico activo: {activeMetric}</p>
</main>
);
}Cuando se envía una Promesa de parámetros a un Client Component, se desenrolla síncronamente durante el renderizado con la primitiva nativa de React 19 —el hook use():
'use client';
import { use } from 'react';
interface ClientSectionViewProps {
paramsPromise: Promise<{ section: string }>;
}
export function ClientSectionView({ paramsPromise }: ClientSectionViewProps) {
// Desenvolvimiento de la promesa de parámetros durante el renderizado del cliente
const { section } = use(paramsPromise);
return (
<div className="p-4 rounded-lg bg-bg-surface border border-border">
<span className="text-xs text-text-muted uppercase">Segmento de cliente:</span>
<h3 className="text-lg font-mono font-bold text-accent-cyan">{section}</h3>
</div>
);
}Durante una navegación suave en cliente (<Link href="...">), el navegador no solicita un nuevo documento HTML. En su lugar, el enrutador envía una petición HTTP en segundo plano con encabezados de control:
RSC: 1 — Indica al servidor que retorne un flujo serializado del grafo de componentes en lugar de HTML plano.Next-Router-State-Tree — Envía una instantánea codificada del árbol de segmentos montado en el navegador. El servidor calcula el Ancestro Común Más Bajo (LCA) y ejecuta exclusivamente los componentes que han cambiado.Inspeccione el protocolo crudo de serialización de React Server Components
Solicita Flight stream en lugar de HTML completo
Envía el árbol del cliente para reconciliación
Bandera de precarga enviada por <Link>
1:I["./components/MetricCard.tsx",["chunks/metric_card_client.js"],"default"]0:{"name":"AnalyticsPage","children":["$","div",null,{"className":"p-6","children":["$","$L1",null,{"metric":"visitors"}]}]}2:L1:{"value":14280,"status":"fresh","timestamp":1741859200}El servidor no envía el código fuente de MetricCard, sino un puntero al chunk de bundle que el navegador descarga e hidrata.
El restante 90% del código permanece en el servidor sin engordar el bundle JS (Zero-Bundle Size).
En Next.js 15, la caché en memoria del navegador (Client Router Cache) se rediseñó para solventar problemas de datos desactualizados:
loading.tsx: Los esqueletos de streaming se almacenan en caché durante 5 minutos, evitando parpadeos de spinners en visitas sucesivas.La configuración de retención de caché se define en next.config.ts:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
staleTimes: {
dynamic: 30, // Mantener rutas dinámicas en memoria durante 30 segundos
static: 180, // Mantener segmentos estáticos durante 3 minutos
},
},
};
export default nextConfig;prefetch={null} (por defecto): Descarga el Flight Payload completo para rutas estáticas. En rutas dinámicas, descarga únicamente el esqueleto hasta el loading.tsx más próximo.prefetch={true}: Fuerza la precarga del árbol completo, incluyendo datos dinámicos.prefetch={false}: Deshabilita por completo la precarga al entrar en el viewport y al pasar el cursor (hover). Los desarrolladores pueden ejecutar router.prefetch() de forma manual.¿Por qué el enrutador de cliente envía el encabezado Next-Router-State-Tree en cada navegación?
Ubicar componentes, server actions y tipos directamente junto a la ruta previene el desorden en carpetas globales y protege los archivos privados de exponerse por error como endpoints públicos.
interface Invoice {
id: string;
amount: number;
status: 'paid' | 'pending';
}
interface InvoiceListProps {
items: Invoice[];
}
export function InvoiceList({ items }: InvoiceListProps) {
return (
<ul className="divide-y divide-border border border-border rounded-lg bg-bg-surface">
{items.map((invoice) => (
<li key={invoice.id} className="p-3 flex justify-between items-center text-sm">
<span className="font-mono text-accent-cyan">#{invoice.id}</span>
<span className="font-semibold">${(invoice.amount / 100).toFixed(2)}</span>
</li>
))}
</ul>
);
}import { InvoiceList } from './_components/InvoiceList';
async function getInvoices() {
return [
{ id: 'inv-101', amount: 4900, status: 'paid' as const },
{ id: 'inv-102', amount: 12000, status: 'pending' as const },
];
}
export default async function BillingPage() {
const invoices = await getInvoices();
return (
<section className="space-y-4">
<h2 className="text-xl font-bold">Gestión de Facturación</h2>
<InvoiceList items={invoices} />
</section>
);
}En page.tsx y layout.tsx, define de forma declarativa las estrategias de ejecución y caché para el subárbol:
// Forzar renderizado dinámico en cada petición entrante
export const dynamic = 'force-dynamic';
// Aislar el entorno de ejecución (Runtime Node.js)
export const runtime = 'nodejs';
// Tiempo de vida de la caché del segmento en segundos (Incremental Static Regeneration, ISR)
export const revalidate = 300;
export default async function ReportsPage() {
return <div className="p-6">Informes ejecutivos (actualizados cada 5 minutos)</div>;
}Durante una navegación suave (<Link>), el enrutador conserva en memoria el estado activo de ranuras @slot no coincidentes. Sin embargo, al recargar la página (F5), la memoria del cliente se borra. Si una ranura no dispone de una página para la URL destino y no se ha definido un archivo default.tsx, Next.js interrumpe el renderizado y retorna un error 404 irrecuperable.
Solución: Declara siempre un archivo de rescate default.tsx en la raíz de cada ranura de ruta paralela.
Marcar app/layout.tsx como componente de cliente para inyectar proveedores de tema o contextos de autenticación transforma todo el subárbol de la aplicación en Client Components. Esto anula el streaming en servidor, bloquea el acceso directo a bases de datos desde páginas hijas y aumenta drásticamente el bundle de JavaScript.
Solución: Aísla los proveedores de cliente en envoltorios hoja (como components/providers/ThemeProvider.tsx) y pasa las páginas de servidor mediante el patrón de ranura {children}.
Colocar route.ts (ruta de API) en la misma carpeta que page.tsx genera un error fatal de compilación en Turbopack, ya que un mismo segmento no puede responder con una interfaz HTML y un flujo HTTP directo (JSON) simultáneamente.
Solución: Separa los endpoints de API y las páginas en ramas de directorio independientes (app/api/users/route.ts y app/users/page.tsx).
En entornos locales sobre sistemas no sensibles a mayúsculas (Windows NTFS o macOS APFS), rutas como app/Profile/page.tsx y app/profile/page.tsx pueden resolver sin errores aparentes. Al desplegar en contenedores Linux (Vercel, Docker), esta discrepancia provoca errores 404 o corrupción de caché de compilación.
Solución: Utiliza estrictamente kebab-case en minúsculas (app/user-profile/page.tsx) para todas las carpetas y activa la regla de ESLint check-file/folder-naming-convention.