wandres.dev
LAZY LOADING · cargar bajo demanda

Splitting por ruta: el corte más rentable

La ruta es la frontera natural de división de una aplicación: quien está en la portada no necesita el código de los ajustes. Dividir el bundle por ruta y cargar cada pantalla al navegar es el patrón de lazy loading con mejor relación esfuerzo-ganancia. Cómo lo automatizan los routers por archivos, cómo se hace a mano, y por qué el SSR y la hidratación encajan de forma natural.

⏱ 13 min

Si solo pudieras aplicar una división a tu bundle, sería esta. Una aplicación tiene cuarenta pantallas y el usuario empieza en una; enviarle las cuarenta para que vea la primera es el desperdicio más grande y a la vez el más fácil de eliminar. La ruta —cada URL de tu aplicación— es una frontera que el propio dominio te regala: nítida, estable y alineada con lo que el usuario realmente hace, que es navegar de una pantalla a otra. Dividir por ruta convierte un monolito en un conjunto de trozos que se cargan justo cuando se pisan. Es el corte con mejor retorno de todo el track.

🎯 Al terminar esta lección sabrás
  • Entender por qué la ruta es la unidad de división más rentable de una aplicación.
  • Ver cómo los routers por archivos convierten cada pantalla en un chunk automáticamente.
  • Declarar rutas diferidas a mano cuando controlas tú el enrutado.
  • Situar el SSR y la hidratación dentro del splitting por ruta sin descargar de más.

La ruta como frontera natural

Un buen punto de corte cumple tres condiciones: separa código que se usa junto, aísla código que rara vez se usa a la vez, y coincide con una decisión que el usuario toma explícitamente. La ruta las cumple las tres a la perfección. Todo lo que necesita la pantalla de ajustes vive bajo esa ruta; nada de eso hace falta en la portada; y el usuario decide cargarla con un acto deliberado, que es navegar hasta ella.

Comparado con dividir por componente, que exige juicio caso por caso, el splitting por ruta es casi mecánico: traza un corte en cada pantalla y ya has eliminado la mayor parte del JavaScript que sobra en el arranque. Por eso es el primer lazy loading que cualquier aplicación debería tener, y el que más mueve la aguja con menos trabajo.

Hay además un argumento de distribución estadística. La mayoría de los usuarios visita un puñado de rutas por sesión, no las cuarenta; y muchas pantallas —el panel de administración, la configuración avanzada, el flujo de exportación— las abre una fracción minúscula del público. Cargar por adelantado el código de rutas que el usuario mediano nunca verá es pagar en el arranque por un valor que casi nadie consume. El splitting por ruta convierte ese gasto fijo en un gasto bajo demanda, proporcional al uso real.

Conviene un matiz para no sobrevender: “el más rentable” no significa “suficiente”. El corte por ruta se lleva el grueso, pero dentro de una pantalla pesada aún puede sobrar código que no se ve al entrar —un editor, un panel oculto—, y eso pide el corte fino del nivel siguiente. La ruta es el primer tajo, el que más quita con menos esfuerzo; rara vez es el último.

flowchart TD
subgraph Bundle monolitico
  M[Un archivo con las 40 rutas] --> M2[Bajas 40 pantallas para ver 1]
end
subgraph Split por ruta
  R[Shell y ruta actual] --> R2[Cada ruta es un chunk aparte]
  R2 --> R3[Al navegar baja solo esa ruta]
end
style M2 fill:#f38ba8,color:#11111b
style R3 fill:#a6e3a1,color:#11111b

Enrutado por archivos: el corte automático

En 2026 la mayoría de frameworks no te piden que dividas por ruta a mano: lo deducen de la estructura de carpetas. Cada archivo dentro del directorio de rutas es, por convención, un punto de entrada distinto, y el bundler lo trata como un import() implícito. El resultado es que el splitting por ruta ocurre sin que escribas una sola línea de configuración.

src/routes/
  index.tsx        -> chunk de la portada
  panel.tsx        -> chunk del panel
  ajustes.tsx      -> chunk de ajustes
  informe/[id].tsx -> chunk del informe

Así funcionan el App Router de Next, Astro con sus páginas, SolidStart, TanStack Router y React Router 7 —la evolución de Remix—, cada uno con su dialecto pero con la misma idea: el árbol de archivos es el mapa de chunks. Cuando el usuario navega, el router carga el chunk de la ruta destino, y solo ese. El código de las otras treinta y nueve pantallas nunca se descarga hasta que se visitan.

Un matiz sobre las rutas dinámicas: un archivo como informe/[id].tsx no genera un chunk por cada identificador, sino un único chunk que sirve a todos los informes. El segmento variable es un parámetro en tiempo de ejecución, no un punto de corte distinto; el código de la plantilla es el mismo se pida el informe que se pida, y por eso se descarga una vez y se reutiliza para cualquier id. Lo que cambian son los datos, no el código.

Cuando controlas tú el enrutado, o cuando quieres un corte que el sistema de archivos no expresa, lo declaras explícitamente. La API moderna de los routers acepta una función que hace el import() por ti, de modo que la definición de la ruta y su punto de corte viven en el mismo sitio.

// React Router 7: cada ruta declara su codigo con lazy, un chunk por pantalla
import { createBrowserRouter } from "react-router";

const router = createBrowserRouter([
  {
    path: "/panel",
    lazy: async () => {
      const { Panel } = await import("./rutas/Panel");
      return { Component: Panel };
    },
  },
]);
💡
El chunk de la ruta arrastra su subárbol

Un detalle que multiplica la ganancia: el chunk de una ruta no contiene solo su componente, sino todo lo que ese componente importa estáticamente y no comparte con el resto. Si la pantalla de informes usa una librería de gráficas de 200 KB y nadie más la toca, esa librería cae dentro del chunk de informes y no pesa un gramo en el arranque. Dividir por ruta arrastra consigo, gratis, una buena parte del splitting por dependencia.

Cortes manuales cuando el árbol no basta

El enrutado por archivos cubre el caso común, pero a veces quieres un corte que la estructura de carpetas no expresa: un widget muy pesado compartido por varias rutas, una variante de una pantalla que solo ven algunos usuarios, un tramo de un formulario largo. Ahí bajas un nivel y colocas el import() a mano dentro de la ruta, combinando el corte grueso automático con cortes finos donde el juicio lo pida.

La mayoría de routers también te dejan afinar cuánto adelantan cada ruta. Una preferencia de precarga por intención descarga el chunk destino en cuanto el usuario muestra que va hacia allí —al enfocar o rozar el enlace—, de modo que el corte por ruta no cueste una espera visible al navegar. Es el puente natural hacia el prefetch del nivel siguiente.

// TanStack Router: adelanta el chunk de la ruta al detectar la intencion
const router = createRouter({
  routeTree,
  defaultPreload: "intent", // precarga al enfocar o rozar el enlace
});

Rutas anidadas y layouts compartidos

Las aplicaciones reales no son una lista plana de pantallas, sino un árbol: un layout de dashboard que envuelve varias sub-rutas, cada una con su propio contenido. Los routers modernos dividen ese árbol por tramos. El layout compartido va a su chunk, y cada ruta hija al suyo, de modo que al moverte entre dos hijas del mismo layout solo se descarga el tramo que cambia: el marco común ya está cargado y se reutiliza. Es code splitting que respeta la jerarquía de la interfaz en lugar de aplanarla.

Esa granularidad por tramos es la que hace que navegar dentro de una sección se sienta instantáneo mientras el primer salto a la sección paga el coste del layout una sola vez. La lección de diseño es colocar en el layout lo verdaderamente común y dejar en cada hoja lo específico, para que los chunks caigan donde el uso los reclama.

SSR, hidratación y el chunk de la ruta

El splitting por ruta encaja con el renderizado en el servidor de una forma que parece diseñada a propósito, y en gran medida lo está. Cuando el usuario pide /panel, el servidor renderiza el HTML de esa ruta y lo envía ya pintado; el navegador lo muestra al instante, antes de ejecutar nada de JavaScript. Después llega la hidratación: el navegador descarga el chunk de esa ruta —solo esa— y engancha los manejadores de eventos sobre el HTML ya presente.

La clave está en qué no ocurre. No se descarga el JavaScript de las otras rutas, porque el servidor solo mandó el HTML de la actual y el cliente solo hidrata lo que ve. El coste de arranque es el shell más una ruta, nunca la aplicación entera. Y con la hidratación selectiva y el streaming de React 19, ni siquiera toda la ruta se hidrata de golpe: las partes interactivas se activan primero y el resto llega en cuanto el hilo tiene hueco.

Qué paga el primer render

Vale la pena hacer explícita la aritmética del arranque, porque es el corazón del ahorro. El servidor manda el HTML de una sola ruta; el cliente descarga el runtime del framework, el shell común y el chunk de esa ruta. Nada más. Una aplicación de página única mal dividida, en cambio, manda un bundle que contiene las cuarenta rutas para renderizar una, y el navegador las parsea todas antes de pintar.

La diferencia se agranda con el tamaño del producto, y esa es la propiedad que hace envejecer bien al patrón. Como el coste ya no escala con el tamaño total de la aplicación sino con el de la ruta visitada, añadir la pantalla número doscientos uno no ralentiza el arranque de las otras doscientas.

Aspecto SPA sin split SSR con split por ruta
JS del primer render toda la app shell más una ruta
Escala con tamaño total tamaño de la ruta
Añadir una pantalla ralentiza a todas no afecta al resto
🗺️

Enrutado por archivos

La estructura de carpetas es el mapa de chunks. Un archivo por pantalla, un chunk por archivo, sin configuración.

🧩

Rutas anidadas

Layouts compartidos en su propio chunk; cada ruta hija en el suyo. Solo se recarga el tramo que cambia al navegar.

Navegación adelantada

El router puede precargar el chunk destino al pasar el ratón sobre un enlace, de modo que al hacer clic ya está listo.

📝
La cascada es el enemigo a vigilar

El único punto flaco de este patrón es la cascada. Si el chunk de una ruta, al ejecutarse, descubre que necesita datos, y esos datos disparan la carga de otro chunk, encadenas esperas en serie: código, luego datos, luego más código, cada paso esperando al anterior. Los routers de 2026 lo combaten cargando en paralelo el código y los datos de la ruta destino desde que se conoce el destino —a menudo en cuanto el ratón toca el enlace— en lugar de esperar a que uno termine para empezar el otro. La regla es descubrir pronto todo lo que la ruta necesita y pedirlo a la vez.

Ese descubrimiento temprano es también la bisagra con el nivel siguiente. Saber qué ruta viene permite adelantar su chunk antes del clic; ahí entran el prefetch y el preload, que convierten una navegación diferida en una navegación que se siente instantánea porque el trabajo ya estaba hecho cuando el usuario decidió hacerlo.

La ruta es donde la arquitectura y el rendimiento hablan el mismo idioma

Hay algo profundo en que la mejor frontera de división sea también la frontera con la que ya organizas tu aplicación. No es casualidad ni suerte: es que la ruta captura una junta real del sistema, un punto donde el acoplamiento cae de forma natural. El código de una pantalla está fuertemente unido por dentro y débilmente unido con el de las demás, porque así es como los productos se dividen en la mente del usuario y, por tanto, en la del equipo que los construye. Cuando un límite de módulo coincide con un límite de producto, dividir por ahí no introduce ninguna arbitrariedad: el corte ya estaba, solo lo estás materializando en el grafo de chunks. Esto explica por qué el splitting por ruta gana casi siempre a los cortes más ingeniosos que uno pueda inventar dentro de una pantalla. Los cortes artificiales pelean contra el acoplamiento; el corte por ruta cabalga sobre una junta que el dominio ya te dio. Y hay una lección más general escondida aquí, que reaparecerá en los monorepos y en el caching: las mejores optimizaciones de build no se imponen sobre la estructura del código, sino que descubren la estructura que ya existe y la respetan. Un sistema bien modularizado se divide bien casi solo, porque sus fronteras de carga y sus fronteras de significado son la misma línea. Cuando te cueste dividir un bundle, sospecha antes de tu arquitectura que de tu bundler: un grafo que no se deja cortar limpiamente por ruta suele estar denunciando un acoplamiento que también te duele en todo lo demás. El lazy loading por ruta no es solo una técnica de rendimiento; es un test de si tu aplicación está dividida por donde debe. Optimizar y ordenar resultan ser, aquí, la misma tarea vista desde dos ángulos.

⚔️ Divide tu aplicación por sus costuras
  1. Lista las rutas de tu aplicación y, para cada una, nombra una dependencia pesada que solo ella use: candidata a caer en su chunk.
  2. Ejecuta un build de producción y localiza en la salida los chunks por ruta; comprueba que la portada no arrastra el código de las pantallas internas.
  3. Si tu router es por archivos, mueve una pantalla pesada a su propio archivo de ruta y confirma que el bundle inicial adelgaza.
  4. Abre la pestaña de red, navega entre dos rutas y observa cómo se descarga el chunk destino solo al navegar, no antes.
  5. Navega entre dos hijas de un mismo layout y verifica que solo baja el tramo que cambia, no el marco compartido.
  6. Busca una cascada: una ruta cuyo chunk pida datos que a su vez carguen más código, y razona cómo paralelizar esa doble espera.