wandres.dev
TREE SHAKING · eliminar código muerto

sideEffects en package.json: la promesa que poda módulos enteros

Aunque no uses ninguno de sus exports, un módulo puede tener efectos secundarios —un import de CSS, un polyfill, el registro de un global— y por eso el bundler, por prudencia, lo conserva. El campo sideEffects en package.json es la promesa formal de que importar los módulos de un paquete no provoca efectos observables, lo que autoriza al bundler a podar módulos completos, no solo exports sueltos. Cómo funciona el booleano y la lista de globs, por qué los barriles de reexport lo necesitan, y el peligro de mentir.

⏱ 15 min

El tree shaking que has visto hasta aquí poda exports no usados, pero se topa con un muro: ¿y si un módulo, además de exportar cosas, hace cosas con solo importarlo? Un import './estilos.css', un polyfill que parchea un prototipo, una llamada que registra un componente global. Ante esa posibilidad, el bundler es prudente por obligación: conserva el módulo aunque no uses ninguno de sus exports, porque eliminarlo podría borrar un efecto que alguien esperaba. El campo sideEffects en package.json es cómo un paquete rompe ese empate declarando, bajo su responsabilidad, que importar sus módulos no tiene efectos observables —y así autoriza al bundler a podarlos enteros.

🎯 Al terminar esta lección sabrás
  • Entender por qué un efecto secundario obliga al bundler a conservar un módulo aunque sus exports no se usen.
  • Declarar sideEffects: false como promesa de que un paquete es seguro de podar por completo.
  • Usar la lista de globs para marcar los archivos que sí tienen efectos y deben conservarse.
  • Reconocer el papel de los barriles de reexport y el peligro de declarar mal el campo.

El problema: exports sin usar pero efectos presentes

El análisis de alcance responde a “¿se usa este export?”, pero esa no es la pregunta completa. Un módulo puede no exportar nada que tú toques y aun así importar ese módulo puede cambiar el estado del mundo: escribir en window, extender Array.prototype, inyectar una hoja de estilos, registrarse en un contenedor de dependencias. Esos son sus efectos secundarios, y ocurren por el mero hecho de que el módulo se evalúe.

Por eso el bundler no puede limitarse a mirar los exports. Si elimina un módulo cuyos exports nadie usa, pero ese módulo tenía un efecto, rompe el programa de una forma silenciosa y difícil de rastrear. Ante la duda, la respuesta correcta es conservadora: mantener el módulo evaluándose por si su sola ejecución importaba. Esa prudencia es sensata, pero también es la que impide podar módulos perfectamente prescindibles.

El dilema del bundler es real y no tiene salida por análisis puro: mirando el código de un módulo, no siempre puede decidir si evaluarlo tiene o no efectos observables. Un efecto puede esconderse tras una llamada opaca, tras un getter, o tras una rama que solo se toma en cierto entorno. Frente a esa incertidumbre, la única política correcta es la pesimista —conservar—, y de ahí que módulos perfectamente inertes acaben en el bundle solo porque el bundler no puede probar que lo son.

// registro.js — no exporta nada util para ti, pero AL IMPORTARSE actua
window.__miApp = window.__miApp || {}
window.__miApp.plugins = []

// polyfill.js — parchea un prototipo con solo evaluarse
if (!Array.prototype.at) {
  Array.prototype.at = function (i) { /* ... */ }
}

Los efectos que fuerzan a conservar un módulo forman un catálogo reconocible, y saber identificarlos es la mitad del trabajo, porque son justo los que el campo sideEffects gobierna:

  • Inyectar estilos: un import './x.css' que no exporta nada pero adjunta una hoja al documento.
  • Parchear el entorno: un polyfill que añade un método a un prototipo o a un objeto global.
  • Registrarse en algún sitio: apuntar un componente, un plugin o un handler en un registro compartido.
  • Escribir estado global: inicializar una variable en window, en globalThis o en un singleton.
  • Emitir un efecto observable al importar: telemetría, un log, o abrir una conexión de red.

Todos comparten un rasgo: ocurren al evaluar el módulo, no al llamar a lo que exporta. Esa es exactamente la clase de comportamiento que el bundler no puede descartar por su cuenta, porque eliminarlo cambiaría el comportamiento del programa de una forma que ningún análisis de exports llega a ver.

sideEffects: false, la promesa de pureza

El campo sideEffects de package.json es la forma de un paquete de decirle al bundler: “puedes fiarte, importar cualquiera de mis módulos no provoca efectos observables”. Con esa promesa, el bundler se libera de su prudencia: si ninguno de los exports de un módulo se usa, puede eliminar el módulo entero, no solo los bindings sueltos.

{
  "name": "mi-libreria",
  "sideEffects": false
}

La diferencia práctica es enorme y se ve mejor con el caso canónico: los barriles. Muchas librerías exponen un index.js que reexporta decenas de módulos —export * from './debounce', export * from './throttle', y así cien veces—. Si importas solo debounce, sin sideEffects: false el bundler teme que evaluar los otros noventa y nueve módulos tuviera efectos, así que los conserva. Con sideEffects: false, sabe que puede descartar los noventa y nueve módulos que no alcanzas y quedarse únicamente con el de debounce.

El efecto sobre el tamaño se mide en órdenes de magnitud, no en porcentajes. Una utilidad monolítica sin sideEffects puede colarte cientos de kilobytes por el simple hecho de importar una función, porque su barril conecta con todo y el bundler no se atreve a soltar nada; la misma utilidad con el campo bien puesto entrega esa función y prácticamente nada más. Por eso, cuando evalúas una dependencia por su peso, comprobar si declara sideEffects te dice más que contar sus líneas de código.

ℹ️
Qué prometes exactamente

sideEffects: false no dice “mi código no hace nada”; dice “importar un módulo, por sí solo, no produce efectos que se deban preservar si sus exports no se usan”. Tus funciones siguen pudiendo tener efectos cuando las llamas; lo que prometes es que la evaluación de cada módulo al ser importado es pura. Es una afirmación sobre el nivel superior del archivo, no sobre lo que ocurre cuando alguien invoca lo que exportas.

📝
Lo entienden Rollup, webpack y Rolldown por igual

Aunque el campo sideEffects nació en webpack, hoy es un convenio compartido: Rollup, Vite y Rolldown lo respetan con la misma semántica, y publicar un paquete con el campo bien puesto beneficia a quien lo consuma con cualquiera de ellos. No es una opción atada a una herramienta concreta, sino metadato del propio paquete —vive en el package.json, el terreno neutral que todos leen— y por eso se ha convertido en parte del contrato de facto de cómo se publica una librería podable en 2026.

La lista de globs: cuando algunos archivos sí tienen efectos

Pocos paquetes son totalmente puros. Lo habitual es que casi todo sea podable salvo un puñado de archivos con efectos legítimos: los estilos, un polyfill, un punto de registro. Para eso sideEffects acepta, en vez de un booleano, una lista de patrones que enumera los archivos que sí tienen efectos y deben conservarse siempre.

{
  "sideEffects": [
    "*.css",
    "./src/polyfills.js"
  ]
}

La forma de la lista es un glob por entrada, con la sintaxis habitual de comodines, interpretado contra las rutas de los archivos del paquete. Es deliberadamente escueta: no describe qué efecto tiene cada archivo, solo que lo tiene y, por tanto, que no debe podarse aunque sus exports queden sin usar. Toda la expresividad que necesita es distinguir dos conjuntos, el de lo conservable y el de lo podable.

La semántica es una lista blanca invertida: todo lo que no coincide con esos patrones se considera puro y podable; lo que coincide se preserva aunque nadie use sus exports. El caso más frecuente es "*.css", porque un import './boton.css' no exporta nada pero su efecto —inyectar estilos— es justo lo que quieres. Sin ese patrón, un sideEffects: false demasiado entusiasta eliminaría tus estilos por considerarlos un import sin exports usados.

Conviene entender la lista como un mecanismo de grano fino que preserva la máxima poda. En vez de renunciar al tree shaking de todo el paquete porque tres archivos tienen efectos, aíslas esos tres y dejas que el resto se sacuda con libertad. Es la diferencia entre apagar la optimización por una excepción y acotar la excepción para salvar la optimización.

💡
Empieza por false y añade excepciones, no al revés

La estrategia que menos sorpresas da al publicar una librería es declarar sideEffects: false de entrada y, a partir de ahí, ir sumando a la lista únicamente los archivos que de verdad tienen efectos, según los vas identificando. Es más seguro que la alternativa —marcarlo todo como impuro por miedo— porque te obliga a justificar cada excepción y deja el máximo de superficie podable por defecto. Una buena regla de higiene: cada entrada de la lista debería poder explicarse en una frase, del tipo “este archivo se conserva porque parchea tal prototipo al importarse”.

🧼

sideEffects: false

Promesa de pureza total: cualquier módulo cuyos exports no uses puede eliminarse entero. El caso ideal para librerías de utilidades.

📃

Lista de globs

Enumera los archivos que sí tienen efectos —CSS, polyfills, registros— para conservarlos mientras el resto se poda.

🛢️

Barriles de reexport

Un index que reexporta cien módulos solo se sacude bien si el paquete declara que importarlos es puro.

⚠️

Mentir se paga

Declarar false teniendo efectos hace que el bundler pode algo que importaba, y el bug aparece lejos, en producción.

flowchart TD
A[modulo con exports sin usar] --> B{el paquete declara sideEffects}
B -->|no declara nada| C[se conserva por prudencia]
B -->|false| D[se elimina el modulo entero]
B -->|lista de globs| E{coincide con un patron}
E -->|si| F[se conserva]
E -->|no| D
style D fill:#a6e3a1,color:#11111b
style C fill:#f9e2af,color:#11111b
style F fill:#89b4fa,color:#11111b

El peligro de declarar mal

Como toda promesa que una herramienta se cree, sideEffects es tan peligrosa como útil. Si declaras sideEffects: false en un paquete que tiene un efecto —un polyfill, un registro global, un CSS importado por su efecto— el bundler te tomará la palabra y podará ese módulo cuando nadie use sus exports. El resultado es un bug de los peores: no falla el build, no falla en dev, y solo en producción, cuando el árbol se sacude de verdad, desaparece el efecto y algo deja de funcionar sin un error claro que lo explique.

Lo que vuelve a este fallo tan escurridizo es su distancia respecto a la causa, en el tiempo y en el espacio. Quien declaró mal el sideEffects pudo ser el autor de una dependencia, meses atrás; quien sufre el síntoma eres tú, en producción, viendo cómo un estilo no se aplica o un método no existe, sin nada que apunte al package.json responsable. Los efectos podados no dejan huella en el error: simplemente, algo que debía suceder al importar no sucedió, y hay que deducirlo por ausencia.

La regla es tratar el campo con el rigor de una firma. Marca false solo si estás seguro de que ningún módulo depende de su evaluación; en cuanto haya efectos, enuméralos con la lista de globs en vez de mentir con un booleano. Y recuerda que consumes esta promesa constantemente: cuando importas una dependencia y no se poda como esperabas, muchas veces es porque a ella le falta declarar bien su sideEffects.

Hay una asimetría cruel entre los dos modos de equivocarse, y entenderla fija el sesgo correcto. Si eres demasiado tímido —no declaras sideEffects teniéndolo todo puro— el castigo es un bundle algo más grande: molesto, pero visible y sin consecuencias funcionales. Si eres demasiado audaz —declaras pureza donde hay efectos— el castigo es un bug de producción sin rastro. La timidez cuesta bytes; la audacia cuesta corrección. Ante la duda, el sesgo prudente es nítido: no prometas una pureza que no puedas defender archivo por archivo.

⚠️
El error silencioso del polyfill podado

El fallo prototípico es un archivo polyfills.js que parchea el entorno y que importas por su efecto, no por sus exports. Si tu package.json dice sideEffects: false, el bundler lo verá como un import sin exports usados y lo eliminará: en producción, el método que el polyfill instalaba ya no existe, y el código revienta en los navegadores que lo necesitaban. La cura no es quitar el tree shaking, sino ser honesto: añade "./src/polyfills.js" a la lista de sideEffects para que ese archivo, y solo ese, quede a salvo de la poda.

Un contrato entre quien publica y quien empaqueta, firmado en el package.json

Hay una idea que sideEffects cristaliza y que vale mucho más allá del tree shaking: el bundler no puede probar la ausencia de efectos secundarios en el caso general, así que la optimización se traslada del terreno de la demostración al del contrato. Determinar si evaluar un módulo cualquiera tiene o no efectos observables es, en su forma general, indecidible; ninguna herramienta puede garantizarlo analizando el código, porque un efecto puede esconderse tras un getter, una llamada opaca o una rama que solo se toma en cierto entorno. Ante esa imposibilidad, hay dos salidas. La primera es la prudencia absoluta: no eliminar nada que pudiera tener un efecto, que es segura pero deja sobre la mesa casi toda la poda de módulos. La segunda es un contrato: quien publica el paquete declara que sus módulos son puros, y quien empaqueta confía en esa declaración para podar con agresividad. sideEffects es exactamente ese contrato, escrito en el único lugar que ambas partes comparten, el package.json. Entender esto te cambia el papel: dejas de ser un consumidor pasivo de la magia del bundler y pasas a ser una de las dos firmas del acuerdo. Cuando publicas, tu sideEffects es una promesa que otros consumirán a ciegas, y mentir en ella —declarar pureza donde hay efectos— es sembrar bugs en las aplicaciones de terceros que jamás verás. Cuando consumes, la calidad de tu poda depende de la honestidad de las promesas ajenas, y buena parte de “esta dependencia no se sacude” se resuelve mirando si declaró bien el campo. La lección estructural es que las optimizaciones que no se pueden demostrar se apoyan en contratos, y los contratos solo valen lo que vale la disciplina de quien los firma. sideEffects no es un interruptor mágico de tamaño; es una responsabilidad compartida, y tratarla con la seriedad de una firma es lo que separa un ecosistema que se poda limpio de uno lleno de módulos zombis que nadie se atreve a eliminar.

⚔️ Firma el contrato con cuidado
  1. Publica o simula un paquete con un barril que reexporta varios módulos e importa uno solo; compara el bundle con y sin sideEffects: false.
  2. Añade un archivo con un efecto real —un polyfill— y comprueba que false lo elimina y que incluirlo en la lista de globs lo salva.
  3. Explica con tus palabras qué promete exactamente sideEffects: false sobre la evaluación de un módulo frente a la invocación de sus exports.
  4. Audita el package.json de una dependencia tuya y decide si su declaración de sideEffects es honesta y por qué.
  5. Escribe la lista de sideEffects correcta para una librería que es pura salvo por sus archivos CSS y un único punto de registro.