Che cosa può togliere un minificatore e che cosa non deve toccare
Pubblicato il 19/05/2025 · 18 min di lettura · Strumenti per sviluppatori
Daniel Okonkwo — Sviluppatore front-end e redattore Tech presso OneKitly
Performance web · Formati di file
Verificato su 6 fonti
Un minificatore può fare solo modifiche che preservano la semantica, e i casi difficili sono tutti spazi bianchi che non sono decorazione. In CSS, lo spazio di «div p» è un combinatore discendente: toglilo e il selettore corrisponde a tutt'altro. Gli spazi attorno a >, + e ~ possono sparire — esbuild ha trasformato «.card > footer» in «.card>footer» lasciando «.card .card-title» intatto. Dentro calc(), gli spazi attorno a + e − sono obbligatori: Chrome riporta CSS.supports per calc(100% - 2px) come true e per calc(100%-2px) come false, e assegnare il secondo lascia la proprietà vuota. Gli spazi attorno a * e / sono facoltativi. In HTML, lo spazio fra elementi inline è contenuto renderizzato — due span hanno misurato 36,92 px con uno spazio in mezzo e 27,28 px senza, uno scarto di 9,64 px — ed è pienamente significativo dentro pre e textarea. In JavaScript, l'inserimento automatico del punto e virgola fa sì che unire le righe cambi il comportamento: una funzione il cui return sta da solo sulla sua riga restituisce undefined finché non le unisci, e allora restituisce l'oggetto. E la rinomina si ferma al confine delle stringhe: il mangling delle proprietà ha trasformato una lettura funzionante in undefined. Quanto al guadagno, sulle risorse di questa pagina brotli da solo ha risparmiato il 66,2 % e minificare prima ha aggiunto solo il 18,1 %.
La minificazione deve preservare il significato, e la parte interessante è lo spazio bianco che porta significato: il combinatore discendente, gli spazi dentro calc(), lo stacco fra due elementi inline. Misurato qui su file reali, compreso ciò che brotli ti avrebbe risparmiato comunque.
L'unica regola: l'output deve comportarsi in modo identico
La minificazione è un passaggio di compilazione con un solo contratto: l'output dev'essere osservazionalmente identico all'input. Non simile, non abbastanza vicino — identico in ogni comportamento da cui una pagina possa dipendere. Tutto ciò che un minificatore fa discende da lì, e ogni bug di minificazione è un punto in cui qualcuno ha creduto che un byte fosse decorazione mentre la specifica dice che è un dato.
Quella distinzione spiega perché i minificatori a espressioni regolari sono pericolosi e quelli basati su parser no. Uno strumento che toglie spazi per pattern non sa se un dato spazio separa due token per leggibilità o unisce due token in un significato composto. Uno strumento che tokenizza l'input secondo il modulo CSS Syntax o la grammatica ECMAScript, costruisce un albero e lo riemette non può commettere l'errore: quando stampa qualcosa, il significato è già fissato nell'albero.
Tutto ciò che segue è stato eseguito e non ricordato. I campioni sono stati minificati con esbuild, le dimensioni misurate con lo zlib di node a gzip livello 9 e brotli qualità 11, e le affermazioni su CSS e layout verificate in un Chrome headless.
CSS: lo spazio che è un combinatore
In un selettore, lo spazio fra due selettori composti è il combinatore discendente. «div p» seleziona ogni p dentro un div; «divp» seleziona un tipo di elemento inesistente, e «.card .card-title» seleziona un .card-title dentro un .card mentre «.card.card-title» seleziona un elemento che porta entrambe le classi. Lo spazio è un token, non formattazione, e nessun minificatore corretto lo toglie.
Gli altri tre combinatori sono punteggiatura e gli spazi che li circondano sono gratis. Dando a esbuild le quattro forme «div p», «div>p», «div + p» e «div ~ p», ha restituito esattamente «div p», «div>p», «div+p» e «div~p». Lo spazio discendente è sopravvissuto; quelli attorno a >, + e ~ no, perché quei caratteri sono inequivocabili da soli. Nel foglio di esempio è successo lo stesso con regole reali: «.card .card-title» è passato intatto mentre «.card > footer», «.card + .card» e «.card ~ .aside-note» si sono stretti.
Il caso delle at-rule è più sottile e merita attenzione. La media query «@media (min-width: 600px) and (max-width: 900px)» è uscita come «@media(min-width:600px)and (max-width:900px)». Lo spazio prima di «and» è sparito, perché una parentesi chiusa termina già il token precedente. Lo spazio dopo «and» è rimasto, perché «and(» verrebbe tokenizzato come token di funzione anziché come identificatore seguito da parentesi. Tutta la disciplina in una riga: uno spazio è rimovibile esattamente quando i due token ai suoi lati non possono fondersi in un token diverso.
CSS: gli spazi dentro i valori
calc() è l'esempio più netto, perché il requisito è asimmetrico. La specifica CSS Values impone spazi su entrambi i lati di + e −, perché senza di essi un token come «-2px» viene letto come un'unica dimensione negativa e l'espressione perde il suo operatore. Chrome concorda con precisione: CSS.supports per width e calc(100% - 2px) restituisce true, mentre calc(100%-2px), calc(100% -2px) e calc(100%- 2px) restituiscono tutti false. Assegnare style.width = «calc(100%-2px)» lascia la proprietà come stringa vuota, perché l'intera dichiarazione viene scartata come non valida.
Gli operatori di moltiplicazione e divisione non hanno questo problema, e il browser lo conferma: calc(100%*2) e calc(100%/2) restituiscono entrambi true. Un minificatore che capisse la grammatica potrebbe quindi stringere quei due e non gli altri. In pratica esbuild è conservativo e ha mantenuto ogni spazio in «calc(100% - 2 * var(--gap))» — corretto per il meno, e una piccola occasione persa per l'asterisco.
Altre due categorie di spazio intoccabile sono emerse nella stessa esecuzione. I valori stringa sono letterali: la dichiarazione content: « new » ha mantenuto entrambe le coppie di spazi interni, perché quei caratteri vengono inseriti nel documento. E le proprietà personalizzate sono flussi di token anziché valori analizzati, quindi esbuild ha lasciato «--card-bg: #ffffff» con lo spazio dopo i due punti mentre toglieva lo spazio identico da «color: var(--card-fg)». Il resto del campione mostra cosa guadagna un minificatore quando la grammatica dei valori la capisce: rgba(0, 0, 0, 0.08) è diventato #00000014, #0000ff è diventato #00f, 150ms è diventato .15s, opacity 0.7 è diventato .7, margin: 0 0 8px 0 è diventato margin:0 0 8px, e ::after è diventato :after.
HTML: lo spazio fra elementi inline è contenuto
L'elaborazione degli spazi bianchi in CSS riduce una sequenza di spazi nel flusso normale a un singolo spazio — ma uno spazio, non nulla. Fra due elementi di livello inline quello spazio viene renderizzato e occupa larghezza, quindi cancellarlo sposta il layout. Misurato in un Chrome headless a 16 px monospace, due span adiacenti separati da un a capo nel sorgente finivano a x = 36,92, mentre gli stessi due span scritti senza spazi in mezzo finivano a x = 27,28. La differenza di 9,64 px è esattamente un carattere di spazio, ed è la differenza fra una fila di link che legge «one two three» e una che legge «onetwothree».
Ecco perché i minificatori HTML aggressivi sono configurabili e le loro impostazioni predefinite sono di solito prudenti. Ridurre cinque spazi e due a capo a un solo spazio è sempre sicuro nel flusso normale. Cancellare l'ultimo spazio rimasto fra due box inline non lo è, e un minificatore che lo faccia come regola generale rifluirà in silenzio menu, briciole di pane, liste di tag e icone inline. Qualsiasi strumento che offra di togliere gli spazi fra i tag sta offrendo di cambiare il tuo layout in cambio di byte.
Due elementi sono assolutamente vietati: pre e textarea. Entrambi valgono per impostazione predefinita white-space: pre, quindi ogni spazio, tabulazione e a capo al loro interno viene preservato e renderizzato. La pagina di esempio contiene un blocco di codice indentato e una textarea con spazi iniziali significativi, e al collassatore di spazi usato per la misura è stato necessario dare un'eccezione esplicita per entrambi. Qualsiasi minificatore senza quell'eccezione distrugge in silenzio esempi di codice e campi di modulo precompilati. La stessa cura vale dentro gli elementi script e style, e per l'a capo iniziale subito dopo un tag pre di apertura, che il parser HTML scarta per specifica — sottigliezza che rende gli strumenti fatti in casa sbagliati in entrambe le direzioni.
JavaScript: inserimento automatico del punto e virgola e rinomina
ECMAScript inserisce punti e virgola in certe interruzioni di riga, il che rende un a capo portatore di significato. Il caso canonico è un return da solo sulla sua riga. Eseguire function f(){ return \n { ok: true } } ha restituito undefined, perché subito dopo return viene inserito un punto e virgola. Scrivere lo stesso codice su una riga ha restituito { ok: true }. Un minificatore ingenuo che unisce le righe cambia quindi il valore prodotto dalla funzione. Anche il verso opposto morde: lo spezzone let x = 1 \n ++x valuta x a 2, mentre unire quelle due righe lancia SyntaxError: Invalid left-hand side expression in postfix operation.
Un minificatore basato su parser non può commettere nessuno dei due errori, perché quando stampa il punto e virgola è già deciso. Dando la stessa funzione con il return isolato a esbuild si ottiene function t(){}export const r=void 0; — ha mantenuto la semantica, ha visto che la funzione poteva restituire solo undefined e ha ripiegato la chiamata a void 0. È questa la differenza fra una trasformazione di testo e un compilatore.
L'altro pericolo di JavaScript è la rinomina, e il suo confine è esatto: un minificatore può rinominare tutto ciò di cui vede ogni riferimento, e nient'altro. Le variabili locali e i parametri di funzione rientrano, ed è da lì che viene gran parte del risparmio. I nomi delle proprietà degli oggetti no, perché una proprietà può essere raggiunta da una stringa che il minificatore non può seguire. Dimostrazione: un modulo che restituiva [config.userName, o[«userName»], o[«retryCount»]] ha dato [«ada», «ada», 3] con minificazione semplice, e [«ada», undefined, undefined] una volta acceso il mangling delle proprietà. L'accesso col punto è stato rinominato insieme alla definizione; le due letture per stringa chiedevano ancora i vecchi nomi e non hanno trovato nulla. La stessa trappola coglie tutto ciò che è raggiunto per nome a runtime — accesso con parentesi quadre costruito da una variabile, andate e ritorni JSON, binding di framework ed eval diretto.
Misurato: quanto vale la minificazione dopo la compressione
Minificazione e compressione rimuovono ridondanza che si sovrappone, quindi la seconda a girare sembra sempre meno impressionante. Sui campioni scritti a mano: il CSS è passato da 1 434 a 1 066 byte, un taglio del 25,7 %, ma dopo brotli la coppia era 552 contro 459 — solo il 16,8 %. Il JavaScript è passato da 1 518 a 747 byte, un taglio del 50,8 %, ma 552 contro 395 dopo brotli, il 28,4 %. L'HTML è passato da 1 033 a 782 byte, il 24,3 %, e 322 contro 302 dopo brotli — il 6,2 %, cioè venti byte.
Unire i tre in un unico payload di pagina mette il numero onesto sul tavolo. Le risorse grezze sommano 3 985 byte e brotli le porta a 1 346 — il 66,2 % di risparmio dalla sola compressione, senza alcun passaggio di build. Minificare prima e poi comprimere dà 1 102 byte. Quindi il contributo marginale della minificazione, sopra a un livello di compressione che hai già, è di 244 byte: il 18,1 %. Reale e da prendere, ma di un ordine di grandezza inferiore a quanto la cifra in byte grezzi lasci intendere.
Quattro file reali di questo repository mostrano quanto la risposta dipenda da cosa c'è dentro. globals.css si è ridotto del 68,2 % grezzo e del 70,2 % dopo brotli — spettacolare, e spiegato interamente dal fatto che 2 084 dei suoi 3 393 byte sono commenti, con tutti e tredici i blocchi di regole intatti da entrambi i lati. app-shell.module.css, che è soprattutto dichiarazioni reali, ha dato il 4,4 % grezzo e il 5,8 % dopo brotli. Due moduli TypeScript pieni di contenuto hanno dato il 10,8 % e il 6,5 % grezzo, ma solo il 3,4 % e il 2,3 % dopo brotli, perché un file fatto soprattutto di stringhe letterali non ha quasi nulla che un minificatore possa toccare. Regola pratica: la minificazione rende in proporzione a quanto del tuo file sono commenti, indentazione e identificatori locali lunghi, e non rende nulla sui dati.
Un ordine delle operazioni che funziona
Attiva prima la compressione, perché è la vittoria singola più grande, non richiede alcun passaggio di build e non può rompere nulla. Su questi campioni brotli da solo ha tolto il 66,2 % dei byte. Poi minifica con uno strumento basato su parser per ciascun linguaggio, con le sue impostazioni predefinite. Quindi, solo se hai una ragione misurata, passa alle opzioni aggressive — mangling delle proprietà, rimozione degli spazi fra i tag — e tratta ciascuna come una modifica da testare, perché ognuna è un punto in cui il contratto di preservazione semantica è stato deliberatamente allentato.
Due abitudini valgono più di qualsiasi impostazione del minificatore. Togli commenti e codice morto alla fonte invece di contare sul minificatore perché se ne accorga — globals.css era per il 61 % commenti, e quel solo fatto spiega tutta la sua riduzione del 68 %. E controlla l'output, non la promessa: fai passare il bundle minificato dalla tua suite di test e confronta le dimensioni compresse di entrambi i lati anziché quelle grezze, perché il numero grezzo è quello che lusinga e quello compresso è ciò che i tuoi utenti scaricano davvero.
| Prima | Dopo | Sicuro? | Perché |
|---|---|---|---|
| .card > footer | .card>footer | Sì | Il > è inequivocabile da solo; gli spazi non portano nulla |
| .card .card-title | .card .card-title (invariato) | Non deve cambiare | Lo spazio è il combinatore discendente; toglierlo seleziona un elemento con entrambe le classi |
| calc(100% - 2px) | calc(100% - 2px) (invariato) | Non deve cambiare | Chrome riporta calc(100%-2px) come non supportato e scarta la dichiarazione |
| content: « new » | content:« new » (spazi mantenuti) | Non deve cambiare | Il contenuto delle stringhe viene inserito nel documento alla lettera |
| rgba(0, 0, 0, 0.08) | #00000014 | Sì | L'esadecimale a otto cifre rappresenta esattamente lo stesso colore |
| margin: 0 0 8px 0 | margin:0 0 8px | Sì | L'abbreviazione rispecchia il secondo valore quando il quarto è omesso |
| return da solo sulla sua riga | a capo rimosso da uno strumento testuale | No | La funzione restituiva undefined prima e l'oggetto dopo |
| o.userName e o[«userName»] | il mangling delle proprietà rinomina solo il primo | No | Il risultato misurato è passato da [ada, ada, 3] a [ada, undefined, undefined] |
Domande frequenti
- Se il mio server invia già gzip o brotli, devo ancora minificare?
- Sì, ma aspettati un guadagno molto minore di quanto i numeri grezzi suggeriscano, e fai funzionare prima la compressione. Misurato sulla pagina di esempio, brotli da solo ha portato 3 985 byte di risorse grezze a 1 346 — il 66,2 % risparmiato senza alcun passaggio di build. Minificare prima di comprimere ha raggiunto 1 102 byte, quindi il contributo marginale della minificazione è stato di 244 byte, il 18,1 % in più. Ne vale la pena, e non costa nulla per richiesta una volta che il build lo fa. I due si sovrappongono perché aggrediscono la stessa ridondanza: identificatori lunghi ripetuti, sequenze di indentazione e testo dei commenti sono esattamente ciò che un compressore a dizionario elimina meglio. Dove la minificazione vince nettamente è su ciò che la compressione non può fare, perché è semantico e non testuale — eliminazione del codice morto, ripiegamento delle costanti, rimozione di rami irraggiungibili, accorciamento della sintassi di colori e unità. L'ordine che conta: compressione attiva, poi minificare, poi misurare le dimensioni compresse e non quelle grezze.
- Il mio layout si è spostato dopo aver attivato la minificazione HTML. Perché?
- Quasi certamente perché il minificatore ha tolto spazi fra elementi di livello inline, che sono contenuto renderizzato e non formattazione. L'elaborazione degli spazi in CSS riduce una sequenza di spazi e a capo nel flusso normale a un solo spazio — uno, non zero — e quello spazio superstite occupa larghezza fra due box inline. Misurato in un Chrome headless a 16 px monospace, due span con un a capo in mezzo nel sorgente finivano a x = 36,92, e la stessa coppia senza alcuno spazio a x = 27,28: 9,64 px di differenza, esattamente uno spazio. Su una barra di navigazione, un elenco di tag o una fila di link inline, quella differenza si vede subito e sembra un bug. Cerca un'opzione tipo collapseWhitespace con modalità aggressiva o conservativa, e preferisci quella conservativa. Se non vuoi stacco fra due elementi inline, toglilo in CSS con un contenitore flex o grid, oppure con font-size sul genitore, così il markup resta indipendente dal layout.
- È mai sicuro rinominare le proprietà degli oggetti?
- Solo quando puoi garantire che ogni accesso alla proprietà sia visibile al minificatore, il che in pratica significa adottare una convenzione di denominazione e comunicarla allo strumento. Il pattern comune è fare mangling solo delle proprietà che corrispondono a un pattern, per esempio un trattino basso finale, così i campi interni vengono rinominati e tutto ciò che è pubblico resta intatto. Ciò che lo rompe è qualsiasi accesso per stringa. Dimostrato qui: un modulo che restituiva [config.userName, o[«userName»], o[«retryCount»]] ha dato [«ada», «ada», 3] con minificazione ordinaria e [«ada», undefined, undefined] una volta abilitato il mangling delle proprietà — l'accesso col punto ha seguito la definizione, le due letture per stringa no. Lo stesso modo di guasto copre l'accesso con parentesi quadre costruito da una variabile, le chiavi provenienti da JSON, i template di framework che legano per nome e tutto ciò che si percorre con Object.keys. Poiché la rottura è silenziosa e compare solo sul percorso di codice che usa la stringa, tratta il mangling delle proprietà come un'ottimizzazione che richiede un giro di test completo, non come una casella da spuntare.
- Posso scrivere un minificatore con espressioni regolari?
- Puoi scrivere qualcosa che di solito funziona, che è il peggior risultato possibile, perché i fallimenti sono rari e silenziosi. Un pattern che toglie sequenze di spazi non distingue un combinatore discendente da un'indentazione, non sa che lo spazio prima di un meno dentro calc() è grammaticalmente richiesto, non vede che un a capo prima di una graffa di chiusura è ciò che fa restituire undefined a un return, e non sa che i caratteri fra virgolette sono contenuto. Ognuna di quelle distinzioni richiede di tokenizzare l'input secondo la grammatica reale. Il collassatore di spazi usato per la misura HTML di questo articolo è deliberatamente ingenuo, e ha avuto bisogno di un'eccezione esplicita per pre e textarea prima di produrre output corretto — e non sarebbe comunque sicuro su una pagina con script inline contenenti parentesi angolari dentro le stringhe. La risposta pratica: usa uno strumento basato su parser per ciascun linguaggio e spendi il tuo sforzo sull'input — meno commenti spediti, nessun codice morto, nomi locali più corti dove non danneggiano la leggibilità.
- Perché minificare il mio file pieno di dati non è servito quasi a niente?
- Perché un minificatore può toccare solo la sintassi, e un file di dati è quasi interamente contenuto. Le stringhe letterali devono sopravvivere byte per byte, le chiavi di oggetto usate a runtime non possono essere rinominate, e i numeri sono già corti quanto lo saranno. Le misure di questo articolo mostrano chiaramente lo schema: due moduli TypeScript pieni di contenuto di questo repository si sono ridotti del 10,8 % e del 6,5 % grezzo, ma solo del 3,4 % e del 2,3 % dopo brotli, perché il poco che la minificazione ha tolto era indentazione e punteggiatura che il compressore avrebbe schiacciato comunque. Confrontalo con globals.css, che si è ridotto del 68,2 % — interamente perché 2 084 dei suoi 3 393 byte erano commenti. Se un file di dati è davvero grande, la leva non è la minificazione ma il formato e la consegna: metti i dati dietro un'API perché le pagine prendano solo ciò che mostrano, dividili perché una rotta carichi la propria fetta, o portali fuori dal bundle JavaScript in JSON che il browser analizza più in fretta e mette in cache separatamente.
Articoli che potrebbero interessarti
Tutte le guide →Strumenti correlati
Fonti
- 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
Hai notato un errore in questo articolo?