En las aplicaciones multipágina tradicionales (MPA), hacer clic en un enlace estándar <a href="..."> inicia un ciclo de red completo: el navegador solicita un documento HTML nuevo, destruye el DOM activo y vuelve a descargar y ejecutar todos los scripts y estilos. Esto provoca la pérdida del contexto del usuario (reproductores multimedia en ejecución, estados de formularios no guardados, diálogos abiertos) y degrada las métricas de Core Web Vitals (particularmente Interaction to Next Paint, INP).
Next.js App Router implementa una arquitectura híbrida de enrutamiento. Las transiciones de página se ejecutan como navegación suave (Soft Navigation): el navegador no recarga la página completa y el enrutador del cliente solicita solo el árbol de React Server Components modificado (RSC Flight Stream), conservando los layouts padres y el estado interactivo.
En combinación con un mecanismo inteligente de Prefetching (precarga) y una caché en memoria optimizada, se logran transiciones de pantalla con latencia percibida casi nula (0 ms).
La arquitectura de navegación en Next.js 15+ y React 19 se sustenta en cuatro mecanismos fundamentales:
/dashboard/analytics y /dashboard/settings, el layout raíz y el armazón del panel de control no se vuelven a renderizar. Únicamente se actualiza el slot hoja children.viewport) mediante la API Intersection Observer.El componente <Link> entra en el área visible. Intersection Observer programa una solicitud de precarga en segundo plano.
El navegador envía un GET HTTP con la cabecera Next-Router-Prefetch: 1. El servidor responde con la estructura estática o un esqueleto.
El Flight Payload de RSC se conserva en la memoria del navegador según las reglas de staleTimes (0s para rutas dinámicas, 300s para estáticas).
El usuario pulsa el enlace. El router intercepta el clic (preventDefault) y toma la UI almacenada sin pausa de red.
React monta el nuevo slot children, traslada el foco accesible al inicio del segmento y gestiona la posición del scroll.
¿En qué se diferencia el comportamiento de prefetch={false} en App Router respecto al Pages Router anterior?
<Link>#El componente <Link> de next/link amplía la etiqueta nativa <a> con precarga automatizada y navegación suave en el cliente:
import Link from 'next/link';
export function NavBar() {
return (
<nav className="flex gap-4 p-4 border-b border-border text-sm">
{/* prefetch={null} (predeterminado): precarga adaptativa de estáticos y esqueletos */}
<Link href="/dashboard" className="hover:text-accent-violet transition-colors">
Panel de control
</Link>
{/* prefetch={true}: fuerza la precarga completa de todo el árbol */}
<Link href="/analytics" prefetch={true} className="hover:text-accent-violet transition-colors">
Analítica
</Link>
{/* scroll={false}: navega sin reiniciar el scroll hacia arriba (ideal para pestañas y paginación) */}
<Link href="/settings?tab=security" scroll={false} className="hover:text-accent-violet transition-colors">
Configuración de seguridad
</Link>
</nav>
);
}En los Client Components, la navegación programática se lleva a cabo mediante el hook useRouter(). En React 19 se recomienda integrarlo con useTransition para gestionar un estado de carga no bloqueante (isPending):
'use client';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
export function CategoryFilter() {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleSelect = (category: string) => {
// Transición no bloqueante: la interfaz no se congela al descargar el payload de RSC
startTransition(() => {
router.push(`/catalog?category=${category}`, { scroll: false });
});
};
return (
<div className="flex items-center gap-2">
<button
onClick={() => handleSelect('laptops')}
disabled={isPending}
className="px-3 py-1.5 rounded bg-bg-elevated border border-border text-xs disabled:opacity-50"
>
{isPending ? 'Cargando...' : 'Portátiles'}
</button>
</div>
);
}En Server Components, Route Handlers y Server Actions, las redirecciones se ejecutan mediante funciones de servidor dedicadas que emiten interrupciones controladas:
import { redirect } from 'next/navigation';
async function getSession() {
return null; // Usuario no autenticado
}
export default async function DashboardPage() {
const session = await getSession();
if (!session) {
// Emite NEXT_REDIRECT y devuelve HTTP 307
redirect('/login?from=/dashboard');
}
return <div>Panel Privado Seguro</div>;
}¿En qué se diferencia invocar router.refresh() frente a recargar la página mediante window.location.reload()?
Al alternar pestañas o filtros con parámetros de búsqueda, el salto involuntario del scroll hacia (0, 0) perjudica la experiencia de usuario. La opción scroll={false} soluciona este comportamiento:
'use client';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
export function DashboardTabs() {
const searchParams = useSearchParams();
const currentTab = searchParams.get('tab') ?? 'overview';
return (
<div className="flex border-b border-border space-x-4">
{['overview', 'logs', 'metrics'].map((tab) => (
<Link
key={tab}
href={`/dashboard?tab=${tab}`}
scroll={false} // Evita el salto no deseado del viewport hacia arriba
className={`pb-2 text-sm capitalize transition-colors ${
currentTab === tab
? 'border-b-2 border-accent-violet font-semibold text-text-primary'
: 'text-text-muted hover:text-text-primary'
}`}
>
{tab}
</Link>
))}
</div>
);
}Invocar window.location.href = '/dashboard' rompe el modelo SPA de Next.js, forzando una recarga completa del documento, vaciando la Client Router Cache y descargando de nuevo los scripts.
Solución: Utiliza siempre el componente <Link> para enlaces declarativos o router.push() de useRouter() para transiciones programáticas.
Tras mutar datos en el servidor, el enrutador del cliente podría continuar mostrando datos obsoletos almacenados en la memoria RAM del navegador.
Solución: Dentro del Server Action invoca siempre revalidatePath('/dashboard') o revalidateTag('tag'), o ejecuta router.refresh() en el cliente.