Next.js App Router se declaró estable en la versión 13.4, allá por mayo de 2023. Un año después, con Next.js 14.2 ya como línea principal, los equipos que dieron el salto tienen lecciones concretas que contar. Algunas alegres, otras dolorosas, casi todas ausentes de la documentación oficial. Este artículo recoge lo que venimos viendo en proyectos reales: qué compensa, qué cuesta y cómo abordar la transición sin hundir la confianza del equipo en el framework.
Un cambio de modelo, no de sintaxis
La tentación inicial es tratar App Router como “Pages Router con otra organización de carpetas”. Es un error caro. El nuevo router cambia el modelo mental de toda la aplicación. Los componentes se ejecutan en el servidor por defecto, el fetching de datos se hace con async/await directamente en el árbol de renderizado, los layouts se anidan por estructura de directorios y el caching pasa de ser algo explícito a ser un sistema con capas que hay que entender.
Las convenciones de fichero (page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx, route.ts) sustituyen a pages/* y getServerSideProps. Cada archivo con un nombre reservado tiene un contrato específico con el framework: layout.tsx envuelve todas las rutas de su subárbol manteniendo estado entre navegaciones, loading.tsx genera un Suspense boundary automático, error.tsx actúa como error boundary. No son imports opcionales, son hooks del sistema de rutas.
El fetching natural es probablemente la mejor parte. Desaparecen los getServerSideProps y los hooks de SWR o React Query para el caso común del servidor. Un componente asíncrono resuelve su propia data y el framework se encarga del streaming:
// app/users/[id]/page.tsx
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await db.user.findUnique({ where: { id: params.id } });
const posts = fetchPosts(params.id); // sin await: se resuelve dentro de Suspense
return (
<section>
<UserCard user={user} />
<Suspense fallback={<PostsSkeleton />}>
<PostsList promise={posts} />
</Suspense>
</section>
);
}
Sobre Server Components se ha escrito mucho, pero el matiz práctico es este: eliminan JavaScript del cliente para todo lo que no necesite interactividad. En una página de detalle típica (header, contenido, footer, algún widget interactivo), el bundle enviado al navegador se reduce fácilmente un 30-40% respecto a la misma página en Pages Router. Eso se traduce en TTI más bajos, sobre todo en móviles de gama media.
Lo que duele de verdad
El caching es el mayor dolor de cabeza del primer mes. Next.js cachea agresivamente por defecto: resultados de fetch, páginas completas, segmentos del router. El resultado típico es que un endpoint que antes devolvía datos frescos ahora devuelve algo de hace horas y nadie entiende por qué. Hay cuatro niveles de cache (Request Memoization, Data Cache, Full Route Cache, Router Cache) y cada uno tiene su mecanismo de invalidación. El flag cache: 'no-store' en fetch, export const revalidate = 60, export const dynamic = 'force-dynamic' y las llamadas a revalidatePath() o revalidateTag() desde Server Actions son herramientas distintas para problemas distintos. Aprender cuál usar cuándo lleva semanas.
La frontera entre Server Components y Client Components es la segunda fuente de fricción. Usar useState o useEffect en un componente de servidor lanza un error claro, pero los fallos silenciosos son peores: una librería que importa condicionalmente algo de window, un context provider que falla en hidratación, un hook de terceros que depende de document. La directiva 'use client' marca un componente y toda su descendencia como cliente; colocarla demasiado arriba en el árbol anula buena parte de las ventajas de Server Components.
Los errores que duelen son los de hidratación. Un Server Component que renderiza una fecha formateada con la timezone del servidor y un Client Component que al hidratar formatea con la del navegador producen un desajuste que React detecta y reporta con un mensaje vago. Hay que pensar explícitamente qué datos son estables entre servidor y cliente.
El ecosistema ha ido adaptándose. Material-UI, Chakra UI, Emotion y otras librerías basadas en CSS-in-JS han publicado guías específicas para App Router, normalmente envolviendo la aplicación en un provider marcado como Client Component. Tailwind, al ser compilado, no sufre el problema. Las librerías de formularios como React Hook Form funcionan pero requieren 'use client' en los componentes de formulario.
Los Server Actions merecen una mención aparte. Son mutations que se escriben como funciones asíncronas marcadas con 'use server' y que se pasan directamente al atributo action de un <form>. Eliminan la necesidad de crear API routes para operaciones sencillas, integran progressive enhancement (el form funciona sin JavaScript) y se combinan con revalidatePath para invalidar cache tras una escritura. En proyectos donde las mutations son simples (crear, actualizar, borrar), reducen boilerplate considerablemente. En flujos complejos con validación intrincada, optimistic updates y reintentos, siguen siendo preferibles endpoints explícitos o librerías como tRPC.
Migrar sin detener la máquina
Next.js permite coexistencia: app/ y pages/ conviven en el mismo proyecto. La estrategia realista para cualquier aplicación en producción es incremental. Se mantienen las rutas existentes en Pages Router, las nuevas features se escriben en App Router, y las rutas antiguas se van migrando una a una cuando hay motivo (una reescritura de UI, un bug que justifica tocar el fichero, una refactorización de data). Para un proyecto mediano, de 30 a 60 rutas, esperad entre dos y seis meses de migración real, no de calendario.
El patrón que mejor ha funcionado: empezar por rutas de lectura pura (listados, detalles, landings), dejar para el final las que tengan lógica compleja de cliente (dashboards interactivos, editores, wizards con múltiples pasos). Las rutas de lectura se benefician inmediatamente de Server Components; las interactivas requieren más refactorización y el beneficio es menor.
Un error común es migrar “por obligación” sin plan. Un equipo pequeño, con un deadline ajustado y un producto que funciona en Pages Router, no gana nada migrando ahora. Vercel ha confirmado que Pages Router seguirá recibiendo mantenimiento indefinidamente y muchas aplicaciones grandes del ecosistema siguen sin migrar por decisión consciente.
Despliegue y rendimiento
En Vercel el despliegue es transparente, como cabía esperar. Fuera de Vercel hay dos caminos: self-hosting con output: 'standalone' sobre Node.js en un contenedor (la opción más predecible), o el adaptador OpenNext para Lambda cuando se quiere serverless genuino. Cloudflare Pages todavía no soporta todas las features de App Router; conviene comprobar la tabla de compatibilidad antes de comprometerse.
Los números que medimos en migraciones reales: TTFB mejora típicamente un 30-50% gracias al streaming; el bundle de cliente se reduce un 30-45% si se usa bien Server Components; LCP y FCP mejoran visiblemente en páginas con data fetching. En contra: el build tarda más, el consumo de CPU en el servidor sube porque ahora renderiza más, y los cold starts en entornos serverless son más pesados.
App Router es un salto significativo pero no gratuito. Las ventajas arquitectónicas son reales y medibles, y el modelo de Server Components apunta hacia donde va React en general. Los costes (curva de aprendizaje, caching sutil, ecosistema todavía adaptándose) también son reales y hay que contarlos en la planificación. Para proyectos nuevos en 2024, la recomendación es clara: empezar directamente con App Router. Para proyectos consolidados en Pages Router, migrar con cabeza, ruta a ruta, sin prisa y con métricas que justifiquen cada paso. La deuda técnica de no migrar no existe todavía; la de migrar mal, sí.