wandres.dev
LA ENVIRONMENT API · multi-entorno y SSR

Por qué unifica dev y build: el mismo grafo y el futuro con Rolldown

El cisma histórico de Vite —ESM sin empaquetar en dev, Rollup en build— y cómo la Environment API lo cierra dando a cada entorno el mismo grafo y la misma config en desarrollo y en producción. Hacia dónde va Vite 8 en 2026 con Rolldown, el bundler en Rust que unifica esbuild y Rollup, y el modo de bundle completo.

⏱ 17 min

Vite nació de una dualidad brillante y, a la vez, problemática: en desarrollo servía ESM nativo sin empaquetar para arrancar al instante; en producción empaquetaba con Rollup para optimizar. Dos caminos, dos motores, dos modelos mentales, y en la costura entre ambos anidaban los bugs de “en dev iba y en build no”. La Environment API es la mitad conceptual de la cura —el mismo grafo por entorno en dev y en build—; Rolldown, el bundler en Rust de 2026, es la otra mitad —el mismo motor en ambos lados—. Juntas cierran el cisma.

🎯 Al terminar esta lección sabrás
  • Entender el cisma histórico: dev sin bundle con ESM, build con Rollup, y sus bugs de paridad.
  • Ver cómo cada entorno usa la misma config y el mismo grafo en dev y en build.
  • Usar createBuilder y el hook buildApp para construir varios entornos.
  • Situar Rolldown y el modo de bundle completo como el cierre del cisma en Vite 8.

El cisma: ESM en dev, Rollup en build

La genialidad original de Vite fue no empaquetar en desarrollo. El navegador entiende ESM, así que Vite le sirve tus módulos tal cual, transformados bajo demanda, y el arranque es instantáneo por muchos archivos que tengas. Pero en producción no puedes enviar miles de módulos sueltos: hay que empaquetar, y para eso Vite usaba Rollup. El problema es que eran dos motores distintos con dos comportamientos distintos.

De esa dualidad nacía una clase entera de bugs: algo funcionaba con el ESM sin empaquetar de dev y se rompía con el Rollup de build, o al revés. Las causas eran sutiles —orden de ejecución de plugins, tratamiento de las dependencias precompiladas por optimizeDeps con esbuild frente a cómo Rollup las empaqueta, interoperabilidad entre CommonJS y ESM— pero el patrón era siempre el mismo: dev y build no compartían ni el motor ni el grafo, así que no había garantía de que vieran tu código igual.

// Un clasico: una dependencia CommonJS que interopera distinto en cada fase
import pkg from 'una-lib-cjs'
// en dev, esbuild la precompila y el default llega tal cual;
// en build, Rollup la empaqueta y el default puede venir envuelto.
// El mismo import, dos formas, y un bug que solo aparece en una de las fases.
const cliente = pkg.createClient ?? pkg.default.createClient
🔀

Orden de plugins

Dev aplica los hooks al servir bajo demanda; build, al empaquetar en bloque. Un plugin sensible al orden podía ver el grafo en momentos distintos.

📥

optimizeDeps

En dev, las dependencias se precompilan con esbuild; en build, las empaqueta Rollup. Dos motores tratando el mismo paquete con reglas distintas.

🔗

CommonJS y ESM

La interoperabilidad entre módulos —qué es el default, qué son los named— podía diferir entre servir sin bundle y empaquetar, rompiendo imports.

Ninguno de esos tres focos era un error de nadie: eran la consecuencia inevitable de tener dos motores mirando el mismo código con supuestos ligeramente distintos. Mientras dev y build fueran caminos separados, la única defensa era la disciplina —probar también la build antes de desplegar— porque la herramienta no ofrecía ninguna garantía estructural de que ambos coincidieran.

flowchart TB
src[Codigo fuente] --> dev[dev con ESM sin empaquetar]
src --> build[build con Rollup]
dev --> bug[bugs de paridad en la costura]
build --> bug
style dev fill:#89b4fa,color:#11111b
style build fill:#f9e2af,color:#11111b
style bug fill:#f38ba8,color:#11111b

El mismo grafo por entorno, en dev y en build

La Environment API ataca la mitad estructural del problema. Un entorno no es solo una entidad de desarrollo: se refleja en un DevEnvironment durante vite dev y en un BuildEnvironment durante vite build, y ambos derivan de la misma entrada de config. Las condiciones de resolución de workerd, sus plugins, su tratamiento de los externals: son los mismos objetos en dev y en build, porque describen el entorno, no la fase.

Eso significa que la pregunta “¿se comporta igual mi código en dev y en producción?” deja de depender de la suerte y pasa a estar respondida por diseño para todo lo que el entorno define. El grafo del entorno ssr en dev y el grafo del entorno ssr en build parten de la misma resolución y el mismo pipeline. La costura no desaparece del todo por sí sola —dev aún sirve sin empaquetar y build aún empaqueta—, pero el marco común elimina la fuente principal de divergencia: la config bifurcada.

Este punto es sutil pero decisivo: la Environment API no promete que dev y build sean idénticos en todo, promete que compartan la definición de cada entorno. La resolución, los plugins y los externals dejan de ser dos configuraciones que mantienes en paralelo y rezas por que no deriven; pasan a ser una sola, consumida en dos fases.

La divergencia que nacía de la configuración duplicada —la más común y la más traicionera, porque nadie la escribe queriendo— queda así eliminada por construcción. No es que te esfuerces en mantener dev y build sincronizados; es que ya no hay dos cosas que sincronizar, sino una leída dos veces.

Para construir, la API ofrece un builder que recorre los entornos y los empaqueta uno a uno, cada cual con su config.

import { createBuilder } from 'vite'

const builder = await createBuilder()
// construye CADA entorno con SU config, la misma que uso en dev
for (const name in builder.environments) {
  await builder.build(builder.environments[name])
}

Y desde la config declarativa, el hook buildApp orquesta el orden de construcción cuando los entornos dependen entre sí —por ejemplo, construir el cliente antes que el servidor que referencia sus assets—.

// vite.config.ts
export default defineConfig({
  builder: {
    async buildApp(builder) {
      await builder.build(builder.environments.client)
      await builder.build(builder.environments.ssr)
      await builder.build(builder.environments.workerd)
    },
  },
})

El orden importa porque los entornos pueden depender unos de otros: el manifiesto de assets que produce el cliente suele necesitarse al construir el servidor que los referencia. buildApp te da el control explícito de esa secuencia, en lugar de dejarla al azar de un recorrido de mapa. Es la diferencia entre “construye todo” y “construye esto, luego aquello, porque lo segundo lee la salida de lo primero”.

📝
Mismo objeto, dos fases: esa es la garantía de paridad

La idea a fijar es que DevEnvironment y BuildEnvironment no son dos configuraciones que casualmente se parecen, sino dos proyecciones de una misma definición de entorno sobre dos fases. La config de workerd se escribe una vez; dev la usa para transformar y servir, build la usa para empaquetar. Cualquier divergencia entre desarrollo y producción que provenga de la resolución, los plugins o los externals queda descartada de raíz, porque no hay dos fuentes que puedan discrepar: hay una, leída dos veces.

Rolldown y el cierre del cisma en Vite 8

La Environment API unifica el modelo; Rolldown unifica el motor. Rolldown es el bundler escrito en Rust que en 2026 se convierte en el corazón de Vite 8, y su ambición es fundir en una sola herramienta lo que antes eran dos: esbuild —que Vite usaba para precompilar dependencias en dev— y Rollup —que usaba para empaquetar en build—. Un único bundler, compatible con la API de plugins de Rollup, corriendo a velocidad de Rust sobre el analizador Oxc.

Con un solo motor, la costura entre dev y build se estrecha hasta casi desaparecer. Rolldown es lo bastante rápido como para plantear algo impensable con Rollup: empaquetar también en desarrollo, el llamado modo de bundle completo. Si dev y build usan el mismo bundler y el mismo grafo por entorno, la vieja pregunta de la paridad pierde su sentido, porque ya no hay dos caminos que comparar: hay uno, ejecutado en dos momentos. El ESM sin empaquetar de dev deja de ser una fuente de divergencia y pasa a ser, como mucho, una opción de arranque.

Oxc es la otra mitad de la ecuación en Rust. No es solo un parser: es una familia de herramientas —analizador, resolvedor, transformador, linter— que comparte una representación del código y evita reparsearlo en cada paso. Rolldown se apoya en ese analizador, y el mismo cimiento sirve a oxlint y a oxfmt. La consecuencia es un toolchain donde parsear tu código una vez alimenta el bundling, el linting y el formateo, en lugar de que cada herramienta lo relea con su propia gramática ligeramente distinta.

La migración se pensó para ser gradual. Existe un paquete puente, rolldown-vite, que sustituye el motor de Vite sin cambiar tu config ni tus plugins de Rollup, para que puedas adoptar el bundler en Rust hoy y medir la diferencia antes de que sea el valor por defecto. Esa compatibilidad con la API de plugins de Rollup no es un detalle: es lo que permite que años de ecosistema de plugins sobrevivan al cambio de motor sin reescribirse.

El salto de rendimiento no es cosmético, y su papel en esta historia es habilitador. esbuild en Go ya había llevado el bundling de dependencias a otra liga frente a webpack; Rolldown y Oxc en Rust extienden esa aceleración a todo el pipeline, incluida la parte que Rollup hacía en JavaScript. Que empaquetar un proyecto grande baje de decenas de segundos a unos pocos es, precisamente, lo que convierte en realista la idea antes absurda de empaquetar también en cada guardado durante el desarrollo.

🦀

Rolldown

Bundler en Rust, compatible con los plugins de Rollup. Un solo motor para dev y build, sobre el analizador Oxc.

🧬

Un grafo por entorno

La misma definición de entorno se proyecta en DevEnvironment y BuildEnvironment. Config única, leída en dos fases.

📦

Bundle completo

Rolldown es tan rápido que empaquetar en dev es viable. La discontinuidad entre servir sin bundle y empaquetar se disuelve.

Lo que todavía no se unifica

Conviene calibrar la promesa para no confundir dirección con destino ya alcanzado. La Environment API elimina la divergencia que nacía de la config bifurcada, pero no borra por decreto toda diferencia entre servir y empaquetar. Mientras el modo de bundle completo sea opcional, el vite dev clásico seguirá sirviendo ESM sin empaquetar, y una minoría de bugs podrá aún vivir en cómo el navegador ejecuta módulos sueltos frente a cómo Rolldown los une. La cura estructural está puesta; su última milla depende de que el bundle en dev se generalice.

Hay además fuentes de divergencia que ninguna herramienta puede cerrar sola, porque no son suyas: variables de entorno distintas entre tu máquina y producción, datos reales frente a datos de prueba, latencias y límites del proveedor. La Environment API garantiza que el código que pruebas sea el que despliegas por cada entorno; no garantiza que el mundo alrededor de ese código sea idéntico. Distinguir la paridad de la representación —que sí se conquista— de la paridad del entorno de ejecución completo —que sigue siendo tu responsabilidad— es parte de usar bien la herramienta.

La lectura madura, entonces, es esta: la Environment API y Rolldown cierran las costuras que eran responsabilidad de la herramienta —config bifurcada, motor doble— y te devuelven, nítida, la única costura que siempre fue tuya, la que separa tu mundo del de producción. Es un progreso enorme precisamente porque distingue con claridad lo que la plataforma puede garantizar de lo que solo tú puedes controlar.

⚠️
El bundle en dev acelera, pero no es gratis conceptualmente

Empaquetar en desarrollo estrecha la costura de paridad, pero reintroduce un matiz que el ESM sin bundle había eliminado: entre que guardas y que ves el cambio hay, de nuevo, un paso de bundling, por rápido que sea. La apuesta de Vite 8 es que Rolldown en Rust hace ese paso tan barato que el coste desaparece en la práctica mientras se recupera la paridad. Merece la pena entender el intercambio en lugar de asumirlo: no es “gratis total”, es “tan rápido que el beneficio de paridad domina al coste de empaquetar”.

Unificar dev y build es cerrar la última costura entre lo que pruebas y lo que envías

El arco entero de este nivel converge aquí. Empezamos viendo que el mundo tenía más de dos runtimes y que Vite solo modelaba dos; seguimos reificando el entorno como una frontera de aislamiento; separamos transformar de ejecutar para liberar el runtime; vimos a los frameworks converger en ese primitivo. Todo ello apuntaba, sin decirlo, a una misma meta: que aquello que pruebas en tu máquina sea, hasta el último detalle, aquello que se ejecuta en producción. La Environment API cierra la costura de la configuración —un entorno, una definición, proyectada en dev y en build— y Rolldown cierra la costura del motor —un bundler, en Rust, para ambas fases—. Lo que queda es un modelo donde cada destino de despliegue es un entorno de primera clase con su grafo, y ese grafo es el mismo cuando desarrollas y cuando empaquetas, procesado por el mismo bundler con los mismos plugins y las mismas condiciones. La consecuencia práctica es el final de una era de bugs: los que vivían en la brecha entre ESM sin empaquetar y Rollup, los que vivían entre Node y el edge, los que vivían entre un runner improvisado y el runtime real. No es una mejora incremental de velocidad, aunque la velocidad de Rust la haga posible; es un cambio en la naturaleza de la garantía que la herramienta te ofrece. Antes, “en mi máquina funcionaba” era una disculpa; con un grafo único por entorno y un motor único en ambas fases, tiende a volverse una tautología, porque tu máquina y producción ejecutan lo mismo. Ese es el destino hacia el que Vite ha estado caminando desde que decidió que el entorno merecía ser un objeto: no herramientas más rápidas para tapar costuras, sino una arquitectura donde las costuras dejan de existir. Cuando lo que pruebas y lo que envías convergen en una sola representación, el build deja de ser una fuente de sorpresas y vuelve a ser lo que siempre debió ser: un detalle de implementación en el que puedes dejar de pensar.

⚔️ Cierra el cisma tú mismo
  1. Reproduce mentalmente o en código un bug de paridad clásico: algo que dependa del orden en que dev y Rollup tratan una dependencia.
  2. Explica por qué DevEnvironment y BuildEnvironment derivan de la misma definición y qué divergencias elimina eso.
  3. Escribe un buildApp que construya client, ssr y workerd en el orden correcto y justifica ese orden.
  4. Investiga el estado de Rolldown en Vite 8 y enumera qué dos herramientas previas unifica.
  5. Argumenta por qué el modo de bundle completo hace que la vieja pregunta de la paridad dev/build pierda sentido.