Qué hace un minificador por dentro
Un minificador reescribe tu JavaScript en otro equivalente pero más corto, y lo hace con tres operaciones sobre el AST: borrar lo insignificante, acortar los nombres locales (mangling) y podar el código que nunca se ejecuta. Tres técnicas, un único contrato de equivalencia observable, y unas fronteras que explican por qué no todo se puede acortar.
Minificar es el arte de decir exactamente lo mismo con menos bytes. Un minificador no comprime en el sentido de gzip —no produce un blob opaco que haya que descomprimir después—, sino que reescribe tu código en otro JavaScript válido, equivalente en comportamiento pero mucho más corto. Toda su potencia se apoya en un único contrato, el de la equivalencia observable, y toda su prudencia nace de respetarlo. Entender las tres operaciones que compone —quitar lo insignificante, acortar nombres y eliminar lo muerto— y por qué cada una tiene una frontera es entender cómo un bundle pasa de megabytes a kilobytes sin romperse por el camino.
- Definir la minificación como reescritura equivalente y separarla del empaquetado y de la compresión.
- Nombrar las tres operaciones: espacios y comentarios,
manglingde nombres y eliminación de código muerto. - Entender por qué el
manglingsolo es seguro dentro de ciertos ámbitos y nunca sobre propiedades por defecto. - Ver por qué un minificador opera sobre el AST, no sobre el texto, y qué contrato jamás puede violar.
Un minificador es un compilador de JavaScript a JavaScript. Recibe tu código, produce otro código que hace lo mismo y ocupa menos. Nada más, y nada menos: toda la disciplina está en ese “hace lo mismo”, que tiene un significado técnico preciso. Se confunde a menudo con comprimir, y no lo es: comprimir produce un blob que hay que revertir antes de ejecutar, mientras que minificar produce código que corre tal cual. Y se confunde con empaquetar, y tampoco: el bundler une módulos en un grafo, el minificador aprieta los bytes de lo que ese grafo produjo. Tres oficios vecinos que conviene no mezclar.
Tres operaciones sobre un mismo contrato
El contrato que un minificador jamás puede romper es la equivalencia observable. Lo que el programa devuelve, las excepciones que lanza, sus efectos secundarios y el orden en que ocurren: todo eso debe ser idéntico antes y después. Lo que sí puede cambiar libremente es aquello que ningún observador legítimo del programa debería mirar: los nombres internos de las variables, los espacios en blanco, el texto que devuelve Function.prototype.toString, los números de línea y columna. Esa libertad es, exactamente, la superficie que el minificador explota.
El contrato traza una frontera nítida entre lo intocable y lo libre:
- Se preserva siempre: valores devueltos, excepciones lanzadas, efectos secundarios y su orden.
- Se puede cambiar: nombres internos, espacios, comentarios no legales y el formato del código.
- Zona gris: el texto de
Function.prototype.toString, que casi nada debería inspeccionar en producción. - Regla práctica: si tu código depende en runtime de un nombre o del formato, el contrato lo rompes tú, no el minify.
Sobre ese margen aplica tres operaciones de ambición creciente. Un antes y un después lo dice todo:
// Antes: legible, 172 bytes
export function saludar(nombreDeUsuario) {
const saludoInicial = "Hola, ";
if (true) {
return saludoInicial + nombreDeUsuario;
}
}
// Despues: nombre corto, rama muerta fuera, sin espacios, 41 bytes
export function saludar(o){return"Hola, "+o}
En esas dos líneas caben las tres técnicas. El parámetro nombreDeUsuario se volvió o; la rama if (true) desapareció porque su condición era una constante verdadera; los espacios, los saltos y las llaves redundantes se esfumaron. A eso se suma la compresión de sintaxis: true se reescribe como !0, undefined como void 0, un if/else sencillo se colapsa en un ternario y varias sentencias se funden con el operador coma.
La compresión de sintaxis reúne muchos de esos trucos, todos con el mismo espíritu de decir lo mismo con menos caracteres:
trueyfalsese vuelven!0y!1, más cortos y siempre equivalentes.undefinedse reescribe comovoid 0, dos tokens que ningún reasignado puede alterar.- Un
if/elseque solo devuelve se colapsa en un ternariocondicion ? a : b. - Sentencias consecutivas se funden con el operador coma para ahorrar puntos y coma.
- Las llaves de un bloque de una sola sentencia desaparecen cuando la gramática lo permite.
Quitar lo insignificante
Espacios, saltos de línea y comentarios se borran. Barato y seguro, siempre que se respete la inserción automática de puntos y coma.
Acortar nombres
El mangling renombra variables y parámetros locales a símbolos de una letra, reutilizando a en ámbitos que no se solapan.
Eliminar lo muerto
El código inalcanzable y las variables sin uso desaparecen; 2 + 3 se pliega a 5 y una rama imposible se descarta entera.
La primera operación parece trivial y esconde un matiz: borrar saltos de línea puede activar la inserción automática de puntos y coma y cambiar el significado. Por eso el minificador reinserta los ; necesarios o formatea de modo que el sentido sobreviva. Y preserva a propósito los comentarios legales —los banners de licencia marcados con /*! ... */— porque borrarlos, aunque ahorre bytes, incumpliría términos de terceros.
La frontera del mangling
Acortar nombres es la operación más rentable y también la más delicada, porque solo es segura donde el minificador puede demostrar que un nombre es privado de un ámbito que controla por completo. Una variable local, un parámetro, una función declarada y no exportada: se renombran sin miedo, e incluso se reutiliza la misma letra a en dos ámbitos hermanos que nunca coexisten. Ese análisis de ámbitos es lo que hace posible el ahorro.
Fuera de esa zona segura empiezan las fronteras. Una referencia a un global que el minificador no posee no se puede tocar. Las propiedades —objeto.metodo— no se renombran por defecto, y la razón es profunda: el minificador no puede saber de cuántas formas se accede a metodo. Podrían leerlo con objeto["metodo"], podría llegar por serialización, podría consumirlo código externo que nunca ve. Existe un mangling de propiedades, pero es opt-in y peligroso: exige listas de reservados o una convención de nombres, y un descuido rompe el programa en silencio.
La regla que separa lo renombrable de lo intocable es una sola —¿puede el minificador probar que controla todos los usos de ese nombre?— y de ella salen los casos:
- Seguro: variables locales, parámetros y funciones no exportadas dentro de un ámbito cerrado.
- Con cuidado: los nombres del nivel superior de un módulo, que el bundler renombra pero cuyo contrato externo debe preservar.
- Prohibido por defecto: las propiedades de objetos, que pueden leerse por string, serializarse o consumirse desde fuera.
- Imposible: cualquier nombre alcanzable por
evalowith, que fuerza a abandonar elmanglingdel ámbito entero.
Esa reutilización de nombres cortos entre ámbitos disjuntos es lo que exprime el máximo ahorro:
// El mismo simbolo "a" se reutiliza en ambitos hermanos que nunca coexisten
function primero(usuario) { return usuario.id; } // usuario pasa a ser a
function segundo(carrito) { return carrito.total; } // carrito tambien a
// Minificado: function primero(a){return a.id}function segundo(a){return a.total}
Hay dos construcciones que obligan al minificador a rendirse en su terreno más fuerte. Un eval puede leer o escribir cualquier variable local por su nombre en tiempo de ejecución, y with inyecta un objeto entero en la cadena de ámbitos. En presencia de cualquiera de los dos, el minificador no puede garantizar que renombrar saldoInicial a a sea seguro, porque un eval("saldoInicial") escondido lo referenciaría por su nombre original. La respuesta correcta y honesta es desactivar el mangling en ese ámbito. Que existan estas puertas de escape no es un defecto: es el contrato de equivalencia funcionando, negándose a optimizar lo que no puede probar seguro.
Podar lo muerto no es hacer tree shaking
La tercera operación es la eliminación de código muerto —dead code elimination o DCE— pero conviene no confundirla con el tree shaking que viste en el nivel del bundler. Son parientes con el mismo apellido y distinto alcance. El tree shaking es inter-módulo: recorre el grafo completo de ESM desde los puntos de entrada y descarta los exports que nadie alcanza. La DCE del minificador es intra-módulo: trabaja dentro de una función o un archivo, sobre el flujo de control local.
Ahí poda lo inalcanzable tras un return, la rama de un if cuya condición es constante, la variable que se calcula y nunca se lee. Pliega constantes —2 + 3 se vuelve 5— y simplifica condiciones que siempre dan el mismo resultado. Y respeta las anotaciones /*#__PURE__*/, con las que el autor promete que una llamada no tiene efectos secundarios, de modo que el minificador puede borrarla si su resultado no se usa.
El repertorio de la poda local es acotado pero muy rentable:
- Sentencias tras un
return, unthrow, unbreako uncontinue, que jamás se alcanzan. - Ramas de un
ifcuya condición es una constante conocida en tiempo de compilación. - Variables y funciones locales que se declaran y nunca se leen ni se llaman.
- Expresiones sin efectos secundarios cuyo valor se descarta, incluidas las marcadas como puras.
En la tubería real, la minificación es casi el último paso: el bundler ya resolvió el grafo, aplicó el tree shaking y produjo unos pocos chunks, y solo entonces el minificador reescribe cada chunk para exprimir sus bytes. Ese orden importa, porque el tree shaking retira módulos enteros que el minificador ni siquiera llega a ver, y lo que aterriza en la minificación es ya código vivo. Pensar la minificación como una etapa aislada lleva a atribuirle méritos —o culpas— que en realidad son del paso anterior.
flowchart TD IN[Codigo fuente legible] --> P[Parsear a un AST] P --> W[Quitar espacios y comentarios] P --> M[Acortar nombres locales] P --> D[Eliminar codigo muerto] W --> C[Regenerar el codigo] M --> C D --> C C --> OUT[Bundle minimo equivalente] style IN fill:#89b4fa,color:#11111b style OUT fill:#a6e3a1,color:#11111b
La distinción de alcance es lo que conviene fijar de una vez:
- Tree shaking: inter-módulo, sobre el grafo de ESM, retira exports y módulos que nadie importa.
- DCE del minificador: intra-módulo, sobre el flujo de control local, poda sentencias y variables dentro de lo que sobrevivió.
Las dos podas se componen: el tree shaking del bundler retira módulos y exports enteros que nadie usa; la DCE del minificador limpia por dentro lo que quedó. Y ambas explican por qué el minificador trabaja sobre el AST y nunca sobre el texto con expresiones regulares. Renombrar con seguridad exige resolver ámbitos; podar con seguridad exige razonar sobre alcanzabilidad. El texto plano no da ninguna de las dos cosas, y los minificadores primitivos que lo intentaron se rompían en cuanto un caso límite tocaba su ceguera.
Detrás de la aparente humildad de “quitar espacios” hay una de las ideas más limpias de todo el toolchain: un minificador es un optimizador cuyo techo no lo pone la ambición, sino la prueba. Cada byte que ahorra lo ahorra porque ha demostrado que hacerlo no altera el comportamiento observable, y cada byte que no ahorra —la propiedad que no renombra, la llamada que no borra, el ámbito que abandona ante un eval— es la constancia honesta de un límite en su capacidad de demostración, no una pereza. Por eso las tres operaciones se ordenan por cuánto conocimiento exigen: borrar espacios casi no requiere entender el programa; acortar nombres exige resolver la estructura de ámbitos; eliminar lo muerto exige razonar sobre qué código puede ejecutarse. Interiorizar esto cambia cómo lees un bundle roto tras minificar. La pregunta nunca es “el minificador tiene un bug”, sino “¿qué suposición mía violó su contrato?”: una propiedad accedida por string que él renombró porque le dije que podía, un efecto secundario que marqué como puro sin serlo, un global que él no sabía que yo tocaba desde otro sitio. El minificador cumplió su parte —preservar lo que podía probar— y tú rompiste la tuya —prometerle algo falso—. Quien entiende que la minificación es demostración, y no magia, deja de temer sus resultados y empieza a predecir con exactitud qué sobrevive intacto y qué exige que le desactives una optimización.
- Escribe una función con una rama
if (false), una variable sin usar y un parámetro de nombre largo; minifícala conesbuildotersery localiza las tres operaciones en la salida. - Añade una llamada cuyo resultado no uses, márcala con
/*#__PURE__*/y confirma que el minificador la elimina; quita la anotación y observa que ahora la conserva. - Accede a una propiedad con notación de string —
obj["metodo"]— y activa elmanglingde propiedades sin reservarla: reproduce la rotura y luego arréglala con una lista de reservados. - Introduce un
evalen un ámbito y comprueba que el minificador deja de acortar los nombres locales de ese ámbito. - Compara el tamaño en bytes del archivo original y del minificado, y explica qué fracción del ahorro vino de cada una de las tres operaciones.