wandres.dev
ROLLUP · el modelo de plugins

Rollup: el bundler ESM-first que definió el modelo

En 2015, con webpack ya dominante, Rollup apostó por tratar los módulos de ES como estructura estática y no como un formato de empaquetado más. De ahí nacieron el tree shaking serio y el scope hoisting: una salida plana, sin runtime, casi escrita a mano. Por qué ese diseño ESM-first convirtió a Rollup en el bundler de referencia para librerías, y cómo su modelo mental sobrevive en Vite y Rolldown en 2026.

⏱ 16 min

En 2015, cuando webpack ya reinaba, Rich Harris publicó Rollup con una tesis incómoda: los módulos de ES no son un formato de empaquetado más, sino una estructura estática que un bundler puede analizar y aplanar. De esa apuesta —tratar import y export como declaraciones analizables en tiempo de compilación, y no como llamadas resueltas en tiempo de ejecución— nacieron el tree shaking serio y una salida tan limpia que parecía escrita a mano. Rollup no fue un empaquetador más: definió el modelo mental que Vite y Rolldown heredarían una década después.

🎯 Al terminar esta lección sabrás
  • Entender por qué el diseño ESM-first habilita el análisis estático del grafo.
  • Ver cómo el scope hoisting y el tree shaking producen una salida plana y sin runtime.
  • Comprender por qué Rollup se volvió el bundler de referencia para librerías.
  • Distinguir el nicho de Rollup —librerías— del de webpack —aplicaciones—.

El giro ESM-first

La idea que lo cambió todo es sutil: en un módulo de ES, import y export son declaraciones estáticas, no instrucciones que se ejecutan. Están siempre en el nivel superior, no dentro de un if, y sus nombres son literales, no cadenas calculadas. Eso significa que un bundler puede leer el archivo, sin ejecutarlo, y saber con certeza qué exporta cada módulo y qué importa de los demás. El grafo completo se conoce por análisis, no por ejecución.

Compáralo con CommonJS. Un require('./' + nombre) es una llamada a función en tiempo de ejecución: puede estar dentro de una condición, recibir una ruta calculada, o envolverse en un try. El bundler no puede saber de antemano qué se carga sin ejecutar el programa. Esa dinámica, cómoda para el autor, le cierra la puerta al análisis estático: si no puedes demostrar qué exports se usan, no puedes eliminar los que no.

La diferencia se resume en qué puede saber el bundler antes de ejecutar una sola línea:

  • ESM: import y export viven en el nivel superior, con nombres literales y analizables en el parseo.
  • CommonJS: require es una llamada en runtime, con rutas calculables y ramas condicionales.
  • Consecuencia: solo con ESM el grafo y el conjunto de exports usados se conocen por análisis puro.
  • Efecto: el tree shaking es honesto sobre ESM e inevitablemente parcial y conservador sobre CommonJS.

Rollup partió de la premisa contraria. Al asumir ESM como formato de primera clase, cada export se convierte en un binding rastreable a lo largo del grafo, y cada import en una arista que el bundler dibuja sin ambigüedad. Sobre esa base estática se apoya todo lo demás: no hay tree shaking honesto sin un grafo que se conozca por completo antes de generar una sola línea de salida.

ℹ️
Bindings, no valores: por qué ESM permite lo que CommonJS no

Un export de ESM no copia un valor: expone un enlace vivo a una variable del módulo. El importador siempre ve el estado actual, y las referencias circulares se resuelven porque los bindings existen antes de que corra el código. Esa semántica de enlaces —fijada en el parseo, no en la ejecución— es justo lo que le da al bundler un mapa fiable de quién usa qué. CommonJS, al exportar el objeto module.exports en tiempo de ejecución, no ofrece ese mapa: por eso su tree shaking es siempre parcial y conservador.

Salida plana: scope hoisting y tree shaking

Con el grafo conocido, Rollup hace dos cosas que definieron su firma. La primera es el tree shaking: recorre el grafo desde los puntos de entrada, marca qué exports se alcanzan de verdad, y descarta el resto. No es minificación —eso viene después—, sino eliminación de código muerto a nivel de export, algo que solo es posible porque los bindings son estáticos.

La segunda es el scope hoisting. En vez de envolver cada módulo en una función y coserlos con un require interno, Rollup fusiona todos los módulos en un único ámbito compartido, renombrando lo que colisione. El resultado no lleva runtime: ni __require, ni tabla de módulos, ni closures por archivo. La salida se parece a lo que habrías escrito a mano si hubieras juntado todo tú mismo.

Son dos operaciones distintas encadenadas sobre el mismo grafo conocido:

  • Tree shaking: recorre desde los puntos de entrada y descarta los exports que no se alcanzan.
  • Scope hoisting: fusiona los módulos supervivientes en un solo ámbito, renombrando lo que colisione.
  • Resultado: ni runtime, ni tabla de módulos, ni closures por archivo; código plano y depurable.
// utilidades.js
export function usada() { return 1 }
export function ignorada() { return 2 }

// main.js
import { usada } from './utilidades.js'
console.log(usada())

Rollup analiza que ignorada no se alcanza desde la entrada, la elimina, y aplana los dos módulos en uno solo:

// bundle.js
function usada() { return 1 }
console.log(usada());

No queda rastro de ignorada, no hay envoltura de módulo, no hay coste de arranque. Esa transparencia importa: el bundle es depurable a simple vista, y el consumidor no paga por lo que no usa. Frente al artefacto de webpack de la época —correcto, pero cargado de andamiaje— la diferencia era visible en el primer vistazo al archivo generado.

flowchart TD
E[modulos ESM con imports estaticos] --> A[analisis estatico del grafo]
A --> T[tree shaking elimina exports sin usar]
T --> H[scope hoisting fusiona en un scope]
H --> O[salida plana y legible sin runtime]

Por qué brilló en librerías

Una aplicación y una librería tienen necesidades opuestas, y ahí se explica todo. Una app quiere un dev server, HMR, carga de imágenes y CSS, y un code splitting agresivo para su enrutado: el territorio donde webpack era imbatible. Una librería quiere justo lo contrario —cero runtime propio, un artefacto minúsculo, salida en varios formatos para cualquier consumidor, y que el que la instale pueda a su vez hacer tree shaking de lo que no toque—. Rollup daba exactamente eso.

Esa especialización tuvo una consecuencia histórica curiosa: aunque Rollup nació como bundler de librerías, Vite lo reclutó después como su empaquetador de aplicaciones, envolviéndolo con el code splitting y la gestión de assets que una app necesita. El bundler pensado para publicar terminó siendo, bajo la orquestación de Vite, el motor de producción de medio ecosistema de aplicaciones. La misma calidad de salida que lo hizo ideal para redistribuir resultó igual de valiosa para desplegar.

Por eso el catálogo de proyectos que lo adoptaron es un quién es quién de las librerías del frontend: React, Vue, D3, three.js, Svelte y buena parte de npm publicaron con Rollup. Su salida plana era ideal para redistribuir; su capacidad de emitir esm, cjs y umd en una pasada resolvía el problema de servir a consumidores dispares; y su footprint nulo encajaba con la promesa de una dependencia que no engorda al que la usa.

Las necesidades de una librería explican, punto por punto, por qué el encaje era tan natural:

  • Varios formatos: emitir esm, cjs y umd en una sola pasada para consumidores dispares.
  • Cero runtime: sin andamiaje que engorde el paquete ni cargue el arranque de quien lo instala.
  • Salida legible: un bundle plano que se depura a simple vista y se audita sin fricción.
  • Tree shaking heredado: el consumidor final poda lo que no toca, porque el esm preserva la estructura.
📦

ESM estático

import y export son declaraciones analizables, no llamadas en runtime. El grafo se conoce sin ejecutar nada.

🌳

Tree shaking

Elimina exports que no se alcanzan desde la entrada. Solo es honesto porque los bindings son estáticos.

🪶

Scope hoisting

Fusiona los módulos en un solo ámbito. Sin __require, sin tabla de módulos, sin closures por archivo.

📚

Nicho librería

Multi-formato, footprint mínimo y salida legible: exactamente lo que una dependencia redistribuible necesita.

💡
El campo sideEffects afina lo que se puede podar

El tree shaking es conservador por defecto: si un módulo pudiera tener efectos secundarios al importarse, Rollup no se atreve a eliminarlo. El campo sideEffects en package.json —o el moduleSideEffects que un plugin devuelve por módulo— es tu forma de declarar que un archivo es puro y se puede podar si nadie usa sus exports. Marcar "sideEffects": false en una librería bien construida es lo que permite que el consumidor final borre ramas enteras que no importa.

📝
La idea ganó incluso fuera de Rollup

El scope hoisting fue tan claramente superior que el propio webpack lo adoptó después con su ModuleConcatenationPlugin, y el tree shaking pasó de rareza a expectativa mínima de cualquier bundler serio. Cuando una técnica se copia en el proyecto rival, deja de ser una característica y se vuelve parte del modelo. Eso fue lo que le ocurrió a las ideas de Rollup: no se impusieron por marketing, sino porque describían mejor qué es empaquetar código moderno.

Rollup no ganó una guerra de bundlers: definió el vocabulario

Lo decisivo de Rollup no fue derrotar a webpack —nunca lo hizo en el terreno de las aplicaciones—, sino cambiar la pregunta que todos hacíamos. Antes de Rollup, empaquetar era coser módulos con un runtime que los cargara en orden; después de Rollup, empaquetar es analizar un grafo estático de ESM y destilar de él el mínimo código que de verdad se usa. Esa reformulación es la que sobrevive en 2026. Cuando Vite eligió Rollup para su build de producción, no eligió una herramienta rápida —no lo era—, eligió el mejor artefacto y el ecosistema de plugins que ese modelo había hecho posible. Y cuando Rolldown reescribe el bundler en Rust sobre Oxc, no reinventa el modelo: reimplementa fielmente el de Rollup, porque el modelo ya era correcto y solo faltaba un motor más veloz debajo. Interioriza esto y dejarás de ver los bundlers como cajas negras intercambiables: verás una idea —el grafo estático de ESM como fuente de verdad— y una sucesión de motores que la ejecutan cada vez mejor. Quien aprendió a pensar en exports rastreables, código muerto y salida plana en 2015 sigue pensando bien en 2026, porque aprendió el modelo y no la implementación. Ese es el sentido de empezar el nivel por aquí: Rollup es el idioma; todo lo que viene después son acentos.

⚔️ Comprueba el modelo con tus manos
  1. Escribe dos módulos ESM donde uno exporte dos funciones y el otro importe solo una, empaqueta con Rollup y confirma que la función no usada desaparece del bundle.
  2. Abre la salida y verifica que no hay ninguna envoltura de módulo ni runtime: el código está aplanado en un solo ámbito.
  3. Reescribe el mismo ejemplo en CommonJS con un require calculado y razona por qué el tree shaking ya no puede eliminar nada con certeza.
  4. Añade "sideEffects": false a un package.json de prueba y observa cómo cambia lo que Rollup se atreve a podar.
  5. Explica en dos frases por qué el mismo modelo justifica que Vite usara Rollup en build y que Rolldown lo reimplemente sin cambiarlo.