wandres.dev
CODE SPLITTING · dividir el bundle

Code splitting: qué es y por qué

Un bundle único obliga al usuario a descargar, parsear y ejecutar la aplicación entera antes de interactuar con una sola pantalla. El code splitting rompe ese monolito en trozos que llegan bajo demanda, alineando el código entregado con el que la primera vista realmente necesita.

⏱ 15 min

Un bundler resuelve tu grafo de módulos y, por defecto, lo aplana en un único archivo. Funciona, pero esconde un coste brutal: quien solo quiere ver la pantalla de login descarga, parsea y ejecuta también el editor enriquecido, el dashboard de analíticas y el visor de PDF que jamás abrirá en esa sesión. El code splitting es la técnica de partir ese monolito en fragmentos —los chunk— que llegan al navegador solo cuando hacen falta. No cambia lo que tu app hace; cambia cuándo llega cada parte.

🎯 Al terminar esta lección sabrás
  • Entender qué es el code splitting y qué problema del bundle monolítico resuelve.
  • Descomponer el coste real del JavaScript en sus tres fases: transferencia, parseo y ejecución.
  • Distinguir el camino crítico de arranque de todo lo que puede diferirse.
  • Conectar el splitting con las métricas de arranque que percibe el usuario.

El bundle monolítico y su coste oculto

Cuando el bundler emite un solo archivo, el tamaño de ese archivo crece con toda la aplicación, no con la pantalla que el usuario abre primero. Una SPA mediana en 2026 acumula con facilidad varios cientos de kilobytes de JavaScript ya minificado y comprimido; sin dividir, esos bytes forman una barrera que el navegador debe cruzar entera antes de pintar nada interactivo.

El problema no es solo la descarga. El JavaScript es, byte por byte, el recurso más caro que sirve una web. Una imagen de 300 KB se decodifica en un hilo aparte y aparece; 300 KB de JavaScript tienen que parsearse, compilarse a bytecode y ejecutarse en el hilo principal, el mismo que responde a los clics. Mientras ese hilo está ocupado evaluando código que la primera vista no usa, la interfaz está congelada.

La situación se agrava con el renderizado en servidor. Una app con SSR pinta el HTML en el servidor, pero ese HTML es inerte hasta que el JavaScript del cliente lo hidrata —vuelve a montar los componentes y engancha los listeners—. Si todo el código viaja en un monolito, la hidratación no empieza hasta descargar y parsear el bundle entero, así que la página se ve completa pero muerta durante un intervalo tanto más largo cuanto mayor sea el bundle. El splitting permite hidratar por partes, priorizando lo que el usuario ve y difiriendo el resto.

Hay además una asimetría perversa en cómo crece el monolito. Tu código de aplicación aumenta con cada funcionalidad que añades, pero también lo hace el árbol de dependencias que arrastras: una librería de gráficas, un cliente de mapas, un parser de fechas. Sin dividir, cada una de esas incorporaciones engorda el bundle que todos descargan, incluido quien nunca las usa. El coste de arranque de una feature deja de ser local —de quien la abre— y se vuelve global —de toda la base de usuarios—. Esa externalidad es la que convierte un monolito manejable en uno insostenible sin que ningún commit concreto parezca culpable.

ℹ️
El monolito no es un error, es el punto de partida

Aplanar el grafo en un archivo es el comportamiento por defecto porque es el más simple y, para una landing pequeña, el más eficiente: una petición, cero cascadas. El code splitting es la respuesta cuando la app crece lo bastante como para que ese archivo único empiece a cobrar a todos el precio de todo. Dividir antes de tiempo es optimización prematura; dividir tarde es dejar dinero en la mesa.

Históricamente, esta técnica nació con webpack a mediados de la década de 2010, cuando las SPA crecieron hasta hacer inviable el bundle único, y desde entonces todo bundler serio la trae de fábrica. Lo que ha cambiado en 2026 no es la idea —sigue siendo diferir lo que no hace falta ahora— sino la calidad del automático: Rolldown, el motor en Rust de Vite 8, hace un chunking por defecto tan bueno que la mayoría de proyectos apenas necesita configurarlo. El trabajo del ingeniero se ha desplazado de implementar el splitting a decidir la política: dónde poner las fronteras y cuándo confiar en la herramienta.

Las tres fases del coste del JavaScript

Para razonar sobre qué gana el splitting hay que separar los tres costes que un bundle impone, porque el splitting ataca a los tres pero en distinta medida.

📡

Transferencia

Los bytes que viajan por la red. Depende del tamaño comprimido (gzip o brotli) y de la latencia. Es el coste que primero salta a la vista, pero rara vez el dominante.

🧠

Parseo y compilación

El motor —V8, JavaScriptCore— convierte el texto en bytecode. Es trabajo proporcional a la cantidad de JavaScript, y ocurre aunque el código no llegue a ejecutarse nunca.

⚙️

Ejecución

Correr el código: construir componentes, registrar listeners, hidratar. Ocupa el hilo principal y bloquea la interacción hasta que termina.

La fase que más sorprende es la segunda. Aunque el usuario nunca abra el visor de PDF, si su código está en el bundle inicial, el motor lo parsea y lo compila al arrancar. Ese trabajo es puro desperdicio: pagas por preparar código que esa sesión no ejecuta. El code splitting lo elimina de raíz al no enviar siquiera ese código hasta que se pide.

Los motores modernos intentan mitigar este coste con parseo perezoso: V8 hace una pasada superficial y difiere la compilación completa de cada función hasta su primera llamada. Pero es un paliativo, no una cura, porque incluso esa pasada superficial cuesta tiempo proporcional al tamaño del archivo, y las heurísticas de pre-parseo fallan a menudo, compilando de más código que no se usará. La única forma de no pagar por parsear un módulo es no enviarlo, y esa es exactamente la palanca del splitting: no optimiza el procesamiento del código muerto, lo hace ausente.

📝
Ausente gana a optimizado

Toda la familia de optimizaciones del bundler —minificación, tree shaking, mangling— trabaja sobre código que sí viaja, haciéndolo más barato de transferir y procesar. El code splitting juega otro juego: en vez de abaratar el código, lo retira del camino crítico. Por eso es cualitativamente distinto y por eso, cuando el arranque duele, suele rendir más mover peso fuera del bundle inicial que exprimir un 5 por ciento más de minificación sobre lo que queda.

Puesto en una tabla, se ve cómo el splitting interviene en cada una de las tres fases por la vía de la ausencia, no del abaratamiento:

Fase Lo que paga el monolito Lo que hace el splitting
Transferencia todos los bytes al arrancar envía solo los del camino crítico
Parseo y compilación procesa todo el código incluido no incluye lo diferido, no lo parsea
Ejecución corre módulos que la vista no usa ejecuta solo lo que se invoca

Dividir por el eje del tiempo

La idea central es partir la aplicación en dos conjuntos: lo que la primera vista necesita ahora —el camino crítico— y todo lo demás, que puede llegar después, cuando el usuario navegue a una ruta o abra una función pesada. El camino crítico debe ser lo más pequeño posible; el resto se difiere.

flowchart TD
subgraph Monolito
  M[bundle unico 800 KB parseado entero al arrancar]
end
subgraph Dividido
  E[entry 140 KB camino critico] --> R1[ruta editor 300 KB bajo demanda]
  E --> R2[ruta dashboard 240 KB bajo demanda]
  E --> S[compartido 120 KB cacheable]
end
style M fill:#f38ba8,color:#11111b
style E fill:#a6e3a1,color:#11111b
style S fill:#89b4fa,color:#11111b

El eje de división más rentable es la ruta: en una app con navegación, cada pantalla es una frontera natural. El usuario que aterriza en /login no necesita el JavaScript de /settings. Por debajo de la ruta está el eje del componente: un editor de texto enriquecido o un mapa interactivo son piezas pesadas que conviene diferir aunque vivan dentro de una misma pantalla, cargándose solo cuando el usuario los invoca.

Conviene ver estos ejes como un espectro de granularidad, no como una dicotomía. En un extremo, no dividir nada: el monolito. En el otro, dividir cada módulo: el over-splitting que estudiaremos al final del nivel. Entre ambos hay un continuo, y el arte consiste en elegir el grano adecuado para cada zona de la app —grueso donde el uso es homogéneo, fino donde hay piezas caras y opcionales—. La ruta es el grano que casi siempre acierta; el componente, el que hay que justificar caso por caso.

El splitting tampoco vive solo: se apoya en el tree shaking, la eliminación del código muerto que el bundler hace dentro de cada chunk. Son complementarios y actúan en planos distintos. El tree shaking pregunta “de lo que importas, ¿qué se usa?” y borra lo demás; el splitting pregunta “de lo que se usa, ¿qué hace falta ahora?” y difiere el resto. Un chunk bien formado es uno del que el tree shaking ya quitó lo inerte y que el splitting colocó en el momento correcto de carga.

En la práctica, el paso de una ruta cargada por defecto a una ruta diferida es un cambio de una línea, y todo el nivel siguiente lo dedicaremos a entender qué ocurre por dentro cuando lo escribes:

// Antes: la ruta pesada viaja en el bundle inicial de todos
import Settings from "./rutas/Settings.tsx";

// Despues: la ruta se difiere en su propio chunk, bajo demanda
const Settings = lazy(() => import("./rutas/Settings.tsx"));

El import() es la mitad mecánica del asunto; la otra mitad es la experiencia de espera. Un componente diferido necesita algo que mostrar mientras su chunk viaja, y de ahí las fronteras de carga como Suspense, que declaran el estado intermedio:

// La frontera decide que se ve mientras el chunk del componente llega
<Suspense fallback={<Esqueleto />}>
  <Settings />
</Suspense>

Sin esa frontera, diferir una vista dejaría un hueco en blanco durante la descarga; con ella, el usuario ve un esqueleto y percibe continuidad. Por eso el code splitting y el diseño de los estados de carga son dos caras de la misma decisión: no basta con partir el código, hay que orquestar con gracia el instante en que el trozo aún no ha llegado.

💡
El presupuesto del camino crítico

Una heurística viva en 2026: fija un presupuesto de JavaScript para el arranque —por ejemplo, mantener el bundle inicial por debajo de cierto umbral comprimido— y trátalo como una restricción dura en tu CI. El code splitting es la herramienta principal para respetar ese presupuesto sin renunciar a funcionalidad, porque mueve peso del arranque a la carga diferida en lugar de eliminarlo.

📝
Qué no conviene dividir

No todo merece un corte. El código del primer render, las utilidades diminutas que comparten todas las vistas y cualquier pieza por debajo de unos pocos kilobytes casi nunca justifican un chunk propio: el coste de la petición extra supera el ahorro. Dividir tiene sentido para lo grande, lo opcional y lo poco frecuente; forzarlo sobre lo pequeño y ubicuo solo añade cascadas. La primera decisión de splitting es siempre qué dejar junto.

Lo que mide el usuario

El impacto del splitting se lee en las métricas de arranque. Un bundle inicial más pequeño mejora el FCP y el LCP, porque el navegador cruza antes la barrera de JavaScript y pinta contenido. Reduce el TBT y el INP, porque hay menos código bloqueando el hilo principal durante la carga. Y acorta el TTI, el momento en que la página deja de mentir: se ve lista pero ya responde de verdad.

Ninguna de esas métricas mejora por comprimir mejor los mismos bytes; mejoran porque hay menos bytes que procesar en el momento crítico. Esa es la diferencia entre el splitting y la minificación: la minificación hace el mismo código más pequeño, el splitting hace que parte del código ni siquiera esté presente cuando no toca.

De estas métricas, la que más peso gana en 2026 es el INP, que mide la latencia real de las interacciones a lo largo de toda la vida de la página, no solo en el arranque. Un bundle inicial hinchado la castiga por partida doble: retrasa el momento en que la primera interacción responde y, si el código diferido se carga en mal momento, introduce bloqueos del hilo principal en plena sesión. El splitting bien hecho no solo acelera el primer pintado; reparte el trabajo de JavaScript en el tiempo para que ninguna interacción concreta tope con una pared de compilación.

⚠️
El valle inquietante de la interactividad

Existe un estado engañoso en el que la página parece lista —el contenido está pintado— pero aún no responde, porque el hilo principal sigue ocupado ejecutando el bundle inicial. El usuario hace clic y no pasa nada. Ese desfase entre lo que se ve y lo que responde es de los peores fallos de experiencia, porque rompe la promesa implícita de una interfaz visible. Reducir el bundle de arranque estrecha esa brecha, acercando el momento en que la página deja de aparentar y empieza a cumplir.

Medir bien, eso sí, exige honestidad sobre las condiciones. Un build local en una red de fibra y una CPU de sobremesa oculta casi todos los costes que este nivel describe; el bundle solo revela su verdadero peso en un dispositivo modesto y una red móvil, que es donde vive buena parte de tus usuarios. La regla es medir en el percentil que te importa, no en tu propia máquina, porque el splitting existe precisamente para los usuarios cuyo hardware y red hacen que cada kilobyte del arranque cuente.

El code splitting materializa el principio de alinear coste con uso

Detrás de esta técnica hay un principio que trasciende el frontend: cada usuario debería pagar el coste de lo que usa, no el de lo que la aplicación podría hacer. El bundle monolítico viola ese principio de raíz, porque cobra a quien abre el login el precio íntegro del editor, del dashboard y del visor que nunca tocará. El code splitting no inventa nada nuevo sobre el rendimiento —los bytes siguen costando lo mismo de transferir, parsear y ejecutar—; lo que hace es reorganizar cuándo se paga cada coste, difiriéndolo hasta el instante en que se convierte en valor real para el usuario. Verlo así reencuadra toda la disciplina: dejas de preguntarte cómo hacer tu bundle más pequeño y empiezas a preguntarte qué necesita de verdad la primera interacción, tratando cada kilobyte del camino crítico como un impuesto que cobras a todo el que llega. Y ese cambio de pregunta tiene un corolario arquitectónico: las fronteras de división no son un detalle de configuración que se ajusta al final, sino decisiones de diseño que deberían reflejar la estructura real de uso de tu producto —qué se usa siempre, qué a veces, qué casi nunca—. Un buen mapa de chunks es, en el fondo, un modelo honesto de cómo la gente usa tu aplicación; un mal mapa delata que nunca te hiciste esa pregunta.

⚔️ Mide tu monolito
  1. Ejecuta el build de un proyecto tuyo y localiza el tamaño del bundle inicial: anota los bytes en bruto y los comprimidos con brotli.
  2. En las herramientas de red del navegador, con la caché desactivada y throttling de red media, cronometra cuánto tarda tu app en ser interactiva desde una pantalla de entrada simple.
  3. Identifica una pieza pesada que la primera vista no necesita —un editor, un gráfico, un visor— y estima cuántos kilobytes representa del bundle inicial.
  4. Con un profiler del hilo principal, mira el tiempo de parseo y compilación al arrancar y razona qué fracción corresponde a código que esa sesión no ejecuta.
  5. Escribe el presupuesto de JavaScript que crees razonable para el camino crítico de esa app y compáralo con la cifra real de hoy.