Qué puede quitar un minificador y qué no debe tocar
Publicado el 19/5/2025 · 18 min de lectura · Herramientas para desarrolladores
Daniel Okonkwo — Desarrollador front-end y redactor de Tecnología en Allin
Rendimiento web · Formatos de archivo
Verificado con 6 fuentes
Un minificador solo puede hacer cambios que preserven la semántica, y los casos difíciles son todos espacios que no son decoración. En CSS, el espacio de «div p» es un combinador descendiente: quítalo y el selector coincide con algo completamente distinto. Los espacios alrededor de >, + y ~ pueden irse — esbuild convirtió «.card > footer» en «.card>footer» dejando «.card .card-title» intacto. Dentro de calc(), los espacios alrededor de + y − son obligatorios: Chrome informa CSS.supports para calc(100% - 2px) como true y para calc(100%-2px) como false, y asignar el segundo deja la propiedad vacía. Los espacios alrededor de * y / son opcionales. En HTML, el espacio entre elementos en línea es contenido renderizado — dos spans midieron 36,92 px con un espacio entre ellos y 27,28 px sin él, un desplazamiento de 9,64 px — y es plenamente significativo dentro de pre y textarea. En JavaScript, la inserción automática de punto y coma hace que unir líneas cambie el comportamiento: una función cuyo return está solo en su línea devuelve undefined hasta que la unes, momento en que devuelve el objeto. Y el renombrado se detiene en la frontera de las cadenas: el mangling de propiedades convirtió una lectura que funcionaba en undefined. En cuanto al beneficio, en los recursos de esta página brotli solo ahorró un 66,2 % y minificar antes añadió solo un 18,1 %.
La minificación tiene que preservar el significado, y lo interesante son los espacios que llevan significado: el combinador descendiente, los espacios dentro de calc(), el hueco entre dos elementos en línea. Medido aquí sobre archivos reales, incluido lo que brotli te iba a ahorrar de todas formas.
La única regla: la salida debe comportarse igual
La minificación es un paso de compilación con un solo contrato: la salida debe ser observacionalmente idéntica a la entrada. No parecida, no lo bastante cercana — idéntica en todo comportamiento del que una página pueda depender. Todo lo que hace un minificador se sigue de ahí, y todo fallo de minificación es un sitio donde alguien supuso que un byte era decoración cuando la especificación dice que es dato.
Esa distinción explica por qué los minificadores con expresiones regulares son peligrosos y los basados en analizador no. Una herramienta que quita espacios por patrón no sabe si un espacio dado separa dos tokens por legibilidad o une dos tokens en un significado compuesto. Una herramienta que tokeniza la entrada según el módulo CSS Syntax o la gramática de ECMAScript, construye un árbol y lo reemite no puede cometer el error: cuando imprime algo, el significado ya está fijado en el árbol.
Todo lo que sigue fue ejecutado, no recordado. Las muestras se minificaron con esbuild, los tamaños se midieron con zlib de node en gzip nivel 9 y brotli calidad 11, y las afirmaciones de CSS y de maquetación se comprobaron en Chrome sin interfaz.
CSS: el espacio que es un combinador
En un selector, el espacio entre dos selectores compuestos es el combinador descendiente. «div p» selecciona todo p dentro de un div; «divp» selecciona un tipo de elemento inexistente, y «.card .card-title» selecciona un .card-title dentro de un .card mientras «.card.card-title» selecciona un elemento que lleva ambas clases. El espacio es un token, no formato, y ningún minificador correcto lo quita.
Los otros tres combinadores son puntuación y sus espacios circundantes son gratis. Dándole a esbuild las cuatro formas «div p», «div>p», «div + p» y «div ~ p», devolvió exactamente «div p», «div>p», «div+p» y «div~p». El espacio descendiente sobrevivió; los de alrededor de >, + y ~ no, porque esos caracteres son inequívocos por sí solos. En la hoja de ejemplo pasó lo mismo con reglas reales: «.card .card-title» salió intacto mientras «.card > footer», «.card + .card» y «.card ~ .aside-note» se apretaron.
El caso de las reglas-at es más sutil y merece detenerse. La consulta de medios «@media (min-width: 600px) and (max-width: 900px)» salió como «@media(min-width:600px)and (max-width:900px)». El espacio antes de «and» desapareció, porque un paréntesis de cierre ya termina el token anterior. El espacio después de «and» se quedó, porque «and(» se tokenizaría como token de función en vez de como identificador seguido de paréntesis. Toda la disciplina cabe en una línea: un espacio se puede quitar exactamente cuando los dos tokens a cada lado no pueden fusionarse en un token distinto.
CSS: los espacios dentro de los valores
calc() es el ejemplo más nítido, porque el requisito es asimétrico. La especificación CSS Values exige espacios a ambos lados de + y −, ya que sin ellos un token como «-2px» se lexa como una única dimensión negativa y la expresión pierde su operador. Chrome coincide con precisión: CSS.supports para width y calc(100% - 2px) devuelve true, mientras calc(100%-2px), calc(100% -2px) y calc(100%- 2px) devuelven todos false. Asignar style.width = «calc(100%-2px)» deja la propiedad como cadena vacía, porque la declaración entera se descarta por inválida.
Los operadores de multiplicación y división no tienen ese problema, y el navegador lo confirma: calc(100%*2) y calc(100%/2) devuelven ambos true. Un minificador que entendiera la gramática podría, pues, apretar esos dos y no los otros. En la práctica esbuild es conservador y conservó cada espacio de «calc(100% - 2 * var(--gap))» — correcto para el menos, y una pequeña oportunidad perdida para el asterisco.
Otras dos categorías de espacio intocable aparecieron en la misma ejecución. Los valores de cadena son literales: la declaración content: « new » conservó ambos pares de espacios interiores, porque esos caracteres se insertan en el documento. Y las propiedades personalizadas son flujos de tokens y no valores analizados, así que esbuild dejó «--card-bg: #ffffff» con el espacio tras sus dos puntos mientras quitaba el espacio idéntico de «color: var(--card-fg)». El resto de la muestra enseña lo que gana un minificador cuando sí entiende la gramática de valores: rgba(0, 0, 0, 0.08) pasó a #00000014, #0000ff a #00f, 150ms a .15s, opacity 0.7 a .7, margin: 0 0 8px 0 a margin:0 0 8px, y ::after a :after.
HTML: el espacio entre elementos en línea es contenido
El procesamiento de blancos de CSS reduce una serie de espacios en flujo normal a un único espacio — pero un espacio, no nada. Entre dos elementos de nivel línea ese espacio se renderiza y ocupa anchura, así que borrarlo mueve la maquetación. Medido en Chrome sin interfaz con monospace de 16 px, dos spans adyacentes separados por un salto de línea en el código terminaban en x = 36,92, mientras los mismos dos spans escritos sin blanco entre ellos terminaban en x = 27,28. La diferencia de 9,64 px es exactamente un carácter de espacio, y es la diferencia entre una fila de enlaces que lee «one two three» y otra que lee «onetwothree».
Por eso los minificadores HTML agresivos son configurables y sus valores por defecto suelen ser cautos. Reducir cinco espacios y dos saltos de línea a un espacio es siempre seguro en flujo normal. Borrar el último espacio que queda entre dos cajas en línea no lo es, y un minificador que lo haga como regla general reflujará en silencio menús, migas de pan, listas de etiquetas e iconos en línea. Cualquier herramienta que ofrezca quitar los blancos entre etiquetas está ofreciendo cambiar tu maquetación a cambio de bytes.
Dos elementos están absolutamente prohibidos: pre y textarea. Ambos valen por defecto white-space: pre, así que cada espacio, tabulación y salto de línea dentro de ellos se preserva y se renderiza. La página de ejemplo contiene un bloque de código indentado y un textarea con espacios iniciales significativos, y al colapsador de blancos usado para la medición hubo que darle una excepción explícita para ambos. Cualquier minificador sin esa excepción destruye en silencio ejemplos de código y campos de formulario prerrellenados. El mismo cuidado vale dentro de los elementos script y style, y para el salto de línea inicial justo tras una etiqueta pre de apertura, que el analizador HTML descarta por especificación — sutileza que deja las herramientas caseras equivocadas en ambos sentidos.
JavaScript: inserción automática de punto y coma y renombrado
ECMAScript inserta puntos y coma en ciertos saltos de línea, lo que hace que un salto de línea cargue significado. El caso canónico es un return solo en su línea. Ejecutar function f(){ return \n { ok: true } } devolvió undefined, porque se inserta un punto y coma justo tras return. Escribir el mismo código en una línea devolvió { ok: true }. Un minificador ingenuo que une líneas cambia, pues, el valor que produce la función. Lo inverso también muerde: el fragmento let x = 1 \n ++x evalúa x a 2, mientras unir esas dos líneas lanza SyntaxError: Invalid left-hand side expression in postfix operation.
Un minificador basado en analizador no puede cometer ninguno de los dos errores, porque cuando imprime el punto y coma ya está decidido. Dando la misma función con el return aislado a esbuild se obtiene function t(){}export const r=void 0; — conservó la semántica, vio que la función solo podía devolver undefined y plegó la llamada a void 0. Esa es la diferencia entre una transformación de texto y un compilador.
El otro peligro de JavaScript es el renombrado, y su frontera es exacta: un minificador puede renombrar todo aquello cuyas referencias ve todas, y nada más. Las variables locales y los parámetros de función califican, y de ahí sale la mayor parte del ahorro. Los nombres de propiedades de objeto no, porque una propiedad puede alcanzarse por una cadena que el minificador no puede seguir. Demostración: un módulo que devolvía [config.userName, o[«userName»], o[«retryCount»]] dio [«ada», «ada», 3] con minificación simple, y [«ada», undefined, undefined] al activar el mangling de propiedades. El acceso por punto se renombró junto con la definición; las dos lecturas por cadena seguían pidiendo los nombres viejos y no encontraron nada. La misma trampa atrapa todo lo alcanzado por nombre en tiempo de ejecución — acceso por corchetes construido desde una variable, idas y vueltas por JSON, enlaces de framework y eval directo.
Medido: cuánto vale la minificación tras la compresión
Minificación y compresión eliminan una redundancia solapada, así que la segunda en ejecutarse siempre parece menos impresionante. En las muestras escritas a mano: el CSS pasó de 1 434 a 1 066 bytes, un 25,7 % menos, pero tras brotli el par era 552 frente a 459 — solo un 16,8 %. El JavaScript pasó de 1 518 a 747 bytes, un 50,8 % menos, pero 552 frente a 395 tras brotli, un 28,4 %. El HTML pasó de 1 033 a 782 bytes, un 24,3 %, y 322 frente a 302 tras brotli — un 6,2 %, veinte bytes.
Juntar los tres en una carga de página pone la cifra honesta sobre la mesa. Los recursos en bruto suman 3 985 bytes y brotli los deja en 1 346 — un 66,2 % de ahorro solo por la compresión, sin paso de compilación. Minificar primero y comprimir después da 1 102 bytes. Así que la contribución marginal de la minificación, encima de una capa de compresión que ya tienes, es de 244 bytes: un 18,1 %. Real y digna de tomarse, pero un orden de magnitud menor de lo que sugiere la cifra en bytes brutos.
Cuatro archivos reales de este repositorio muestran cuánto depende la respuesta de lo que hay dentro. globals.css encogió un 68,2 % en bruto y un 70,2 % tras brotli — espectacular, y explicado del todo porque 2 084 de sus 3 393 bytes son comentarios, con los trece bloques de reglas intactos en ambos lados. app-shell.module.css, que es sobre todo declaraciones reales, dio un 4,4 % en bruto y un 5,8 % tras brotli. Dos módulos TypeScript cargados de contenido dieron un 10,8 % y un 6,5 % en bruto, pero solo un 3,4 % y un 2,3 % tras brotli, porque un archivo hecho sobre todo de cadenas literales casi no tiene nada que un minificador pueda tocar. Regla práctica: la minificación paga en proporción a cuánto de tu archivo son comentarios, sangrado e identificadores locales largos, y no paga nada sobre datos.
Un orden de operaciones que funciona
Enciende primero la compresión, porque es la mayor victoria individual, no necesita paso de compilación y no puede romper nada. En estas muestras brotli solo quitó el 66,2 % de los bytes. Después minifica con una herramienta basada en analizador para cada lenguaje, con sus ajustes por defecto. Luego, solo si tienes una razón medida, ve a por las opciones agresivas — mangling de propiedades, eliminación de blancos entre etiquetas — y trata cada una como un cambio que hay que probar, porque cada una es un sitio donde el contrato de preservación semántica se ha relajado a propósito.
Dos hábitos valen más que cualquier ajuste de minificador. Quita comentarios y código muerto en el origen en vez de confiar en que el minificador los note — globals.css era un 61 % comentarios, y ese solo hecho explica toda su reducción del 68 %. Y comprueba la salida, no la promesa: pasa el paquete minificado por tu batería de pruebas, y compara los tamaños comprimidos de ambos lados en vez de los brutos, porque la cifra bruta es la que halaga y la comprimida la que tus usuarios descargan de verdad.
| Antes | Después | ¿Seguro? | Por qué |
|---|---|---|---|
| .card > footer | .card>footer | Sí | El > es inequívoco por sí solo; los espacios no llevan nada |
| .card .card-title | .card .card-title (sin cambios) | No debe cambiar | El espacio es el combinador descendiente; quitarlo selecciona un elemento con ambas clases |
| calc(100% - 2px) | calc(100% - 2px) (sin cambios) | No debe cambiar | Chrome informa calc(100%-2px) como no soportado y descarta la declaración |
| content: « new » | content:« new » (espacios conservados) | No debe cambiar | El contenido de las cadenas se inserta tal cual en el documento |
| rgba(0, 0, 0, 0.08) | #00000014 | Sí | El hexadecimal de ocho dígitos representa exactamente el mismo color |
| margin: 0 0 8px 0 | margin:0 0 8px | Sí | La abreviatura refleja el segundo valor cuando se omite el cuarto |
| return solo en su línea | salto de línea quitado por una herramienta de texto | No | La función devolvía undefined antes y el objeto después |
| o.userName y o[«userName»] | el mangling de propiedades renombra solo el primero | No | El resultado medido pasó de [ada, ada, 3] a [ada, undefined, undefined] |
Preguntas frecuentes
- Si mi servidor ya envía gzip o brotli, ¿hace falta minificar?
- Sí, pero espera una ganancia mucho menor de lo que sugieren las cifras brutas, y pon a funcionar la compresión primero. Medido en la página de ejemplo, brotli solo llevó 3 985 bytes de recursos brutos a 1 346 — un 66,2 % ahorrado sin ningún paso de compilación. Minificar antes de comprimir llegó a 1 102 bytes, así que la contribución marginal de la minificación fue de 244 bytes, un 18,1 % encima. Merece la pena, y no cuesta nada por petición una vez que tu compilación lo hace. Ambos se solapan porque atacan la misma redundancia: identificadores largos repetidos, tiradas de sangrado y texto de comentarios son justo lo que mejor elimina un compresor de diccionario. Donde la minificación gana de plano es en lo que la compresión no puede hacer, por ser semántico y no textual — eliminación de código muerto, plegado de constantes, descarte de ramas inalcanzables, acortamiento de la sintaxis de colores y unidades. El orden que importa: compresión activada, luego minificar, luego medir los tamaños comprimidos y no los brutos.
- Mi maquetación se movió al activar la minificación HTML. ¿Por qué?
- Casi con seguridad porque el minificador quitó espacios entre elementos de nivel línea, que son contenido renderizado y no formato. El procesamiento de blancos de CSS reduce una serie de espacios y saltos de línea en flujo normal a un espacio — uno, no cero — y ese espacio superviviente ocupa anchura entre dos cajas en línea. Medido en Chrome sin interfaz con monospace de 16 px, dos spans con un salto de línea entre ellos en el código terminaban en x = 36,92, y el mismo par sin ningún blanco en x = 27,28: 9,64 px de diferencia, exactamente un espacio. En una barra de navegación, una lista de etiquetas o una fila de enlaces en línea, esa diferencia se ve al instante y parece un fallo. Busca una opción tipo collapseWhitespace con modo agresivo o conservador, y prefiere el conservador. Si no quieres hueco entre dos elementos en línea, quítalo en CSS con un contenedor flex o grid, o con font-size en el padre, para que el marcado siga siendo independiente de la maquetación.
- ¿Alguna vez es seguro renombrar propiedades de objeto?
- Solo cuando puedas garantizar que todo acceso a la propiedad es visible para el minificador, lo que en la práctica significa adoptar una convención de nombres y comunicársela a la herramienta. El patrón habitual es manglear solo las propiedades que coincidan con un patrón, como un guion bajo final, de modo que los campos internos se renombren y todo lo público quede intacto. Lo que lo rompe es cualquier acceso por cadena. Demostrado aquí: un módulo que devolvía [config.userName, o[«userName»], o[«retryCount»]] dio [«ada», «ada», 3] con minificación ordinaria y [«ada», undefined, undefined] al activar el mangling de propiedades — el acceso por punto se movió con la definición, las dos lecturas por cadena no. El mismo modo de fallo cubre el acceso por corchetes construido desde una variable, las claves llegadas de JSON, las plantillas de framework que enlazan por nombre y todo lo que se recorra con Object.keys. Como la rotura es silenciosa y solo aparece en la ruta de código que usa la cadena, trata el mangling de propiedades como una optimización que exige una tanda completa de pruebas, no como una casilla.
- ¿Puedo escribir un minificador con expresiones regulares?
- Puedes escribir algo que suele funcionar, que es el peor resultado posible, porque los fallos son raros y silenciosos. Un patrón que quita tiradas de blancos no distingue un combinador descendiente de una sangría, no sabe que el espacio antes de un menos dentro de calc() es gramaticalmente obligatorio, no ve que un salto de línea antes de una llave de cierre es lo que hace que un return devuelva undefined, y no sabe que los caracteres entre comillas son contenido. Cada una de esas distinciones exige tokenizar la entrada según la gramática real. El colapsador de blancos usado para la medición HTML de este artículo es deliberadamente ingenuo, y hubo que tallarle una excepción explícita para pre y textarea antes de que produjera salida correcta siquiera — y aun así no sería seguro en una página con scripts en línea que contengan ángulos dentro de cadenas. La respuesta práctica: usa una herramienta basada en analizador por lenguaje y gasta tu esfuerzo en la entrada — menos comentarios enviados, nada de código muerto, nombres locales más cortos donde no dañen la legibilidad.
- ¿Por qué minificar mi archivo cargado de datos apenas ayudó?
- Porque un minificador solo puede tocar la sintaxis, y un archivo de datos es casi todo contenido. Las cadenas literales deben sobrevivir byte a byte, las claves de objeto usadas en tiempo de ejecución no se pueden renombrar, y los números ya son lo más cortos que van a ser. Las medidas de este artículo muestran el patrón con claridad: dos módulos TypeScript cargados de contenido de este repositorio encogieron un 10,8 % y un 6,5 % en bruto, pero solo un 3,4 % y un 2,3 % tras brotli, porque lo poco que quitó la minificación era sangrado y puntuación que el compresor iba a exprimir igualmente. Compáralo con globals.css, que encogió un 68,2 % — enteramente porque 2 084 de sus 3 393 bytes eran comentarios. Si un archivo de datos es de verdad grande, la palanca no es la minificación sino el formato y la entrega: pon los datos tras una API para que las páginas traigan solo lo que muestran, divídelos para que una ruta cargue su propia porción, o sácalos del paquete JavaScript a JSON que el navegador analiza más rápido y cachea aparte.
Artículos que podrían interesarte
Todas las guías →Herramientas relacionadas
Fuentes
- W3C — CSS Syntax Module Level 3 — tokenization rules that decide which whitespace is removable
- W3C — CSS Values and Units Module Level 3 — the calc() grammar requiring whitespace around + and −
- W3C — CSS Text Module Level 3 — white space processing and the white-space property
- W3C — Selectors Level 4 — the descendant combinator is whitespace
- WHATWG — HTML Standard — parsing, the pre element's leading newline, and raw text elements
- Ecma International / TC39 — ECMAScript Language Specification — Automatic Semicolon Insertion
¿Has detectado un error en este artículo?