Prima di essere pubblicato, il JavaScript passa per alcune lavorazioni che vengono spesso descritte insieme come «ottimizzazione». Due di esse hanno obiettivi opposti, e confonderle porta a scelte che costano prestazioni senza fornire la protezione che si credeva di comprare.
La catena, per intero#
sorgenti (src/js/*.js) │ ├─ 1. BUNDLE esbuild: risolve gli import, produce un file solo │ → 5 richieste HTTP diventano 1 │ ├─ 2. MINIFICA rinomina le variabili locali, toglie spazi e commenti, │ accorcia le espressioni. NIENTE viene rimosso in modo │ osservabile: il comportamento è identico. │ → ~17 KB │ ├─ 3. OFFUSCA rende il codice deliberatamente ostile alla lettura: │ array di stringhe cifrate, flusso di controllo │ appiattito, codice morto iniettato. │ → ~148 KB (nove volte tanto) │ └─ 4. COMPRIME il server applica zstd o gzip in trasmissione. Non è una fase di build: è una configurazione.Le prime due hanno lo stesso obiettivo — meno byte, più velocità.La terza va nella direzione opposta e persegue un obiettivo diverso.Minificare: togliere#
// Che cosa fa un minificatore, concretamente.// Prima:function calcolaTotaleCarrello(righeCarrello, aliquotaIva) { // Somma i prezzi tenendo conto della quantità let totaleImponibile = 0; for (const riga of righeCarrello) { totaleImponibile += riga.prezzoUnitario * riga.quantita; } return totaleImponibile * (1 + aliquotaIva);}// Dopo:function calcolaTotaleCarrello(t,a){let e=0;for(const l of t)e+=l.prezzoUnitario*l.quantita;return e*(1+a)}// Da notare: i nomi LOCALI sono accorciati, quelli esportati e le// proprietà degli oggetti NO — rinominarli cambierebbe il comportamento// osservabile (una proprietà si può leggere con obj['prezzoUnitario']).// È il motivo per cui la minificazione è sicura per definizione: non// applica nessuna trasformazione che possa cambiare il risultato.Il dettaglio che spiega perché la minificazione è affidabile: i nomi locali vengono accorciati, quelli esportati e le proprietà degli oggetti no. Rinominare una proprietà cambierebbe il comportamento osservabile — si potrebbe leggerla con obj['prezzoUnitario'], o serializzarla in JSON — e un minificatore corretto non applica mai trasformazioni che possano farlo.
Il guadagno che spesso conta di più#
// Il guadagno che spesso supera quello della minificazione: rimuovere// il codice che non viene mai raggiunto. Funziona SOLO con i moduli ES,// perché la loro struttura è analizzabile staticamente.// ✓ Analizzabile: il bundler sa esattamente cosa serveimport { formatta } from './utils.js'; // porta dentro solo formatta// ✗ Non analizzabile: require è una chiamata di funzione, il cui// argomento potrebbe essere calcolato a runtimeconst utils = require('./utils.js'); // porta dentro tutto// ✗ Non analizzabile: l'import dinamico con percorso variabileconst m = await import('./' + nome + '.js'); // il bundler include TUTTO // ciò che potrebbe servire// E l'effetto collaterale dichiarato: se un modulo fa qualcosa al solo// essere importato (registra un polyfill, modifica un prototipo), il// bundler non può rimuoverlo anche se non se ne usa nessun export.// In package.json: "sideEffects": false dice "non ne ho, rimuovi pure".Rimuovere il codice mai raggiunto rende spesso più della riduzione dei nomi, soprattutto quando si importano poche funzioni da librerie grandi. Ma richiede che il bundler possa analizzare staticamente le dipendenze, e ci sono tre modi comuni di impedirglielo: usare require, comporre a runtime il percorso di un import dinamico, e non dichiarare l'assenza di effetti collaterali.
Offuscare: aggiungere#
// L'offuscamento va nella direzione opposta: aggiunge codice per// rendere quello vero difficile da seguire.{ // Le stringhe non stanno più nel codice: vivono in un array // cifrato, e ogni uso passa da una funzione di lookup. stringArray: true, stringArrayEncoding: ['base64'], stringArrayThreshold: 0.75, // Il flusso lineare diventa una macchina a stati con un dispatcher. // È la trasformazione più efficace e la più costosa in esecuzione. controlFlowFlattening: true, controlFlowFlatteningThreshold: 0.5, // Blocchi di codice mai eseguiti, indistinguibili da quelli veri // senza analisi dinamica. deadCodeInjection: true, deadCodeInjectionThreshold: 0.3, // Il codice verifica di non essere stato riformattato: passarlo per // un abbellitore lo rompe. selfDefending: true}Le trasformazioni non riducono niente: aggiungono strutture il cui scopo è rendere il codice difficile da seguire per un lettore umano. L'array di stringhe cifrate elimina il modo più rapido di orientarsi in un file sconosciuto — cercare un testo visibile nell'interfaccia. L'appiattimento del flusso di controllo trasforma una sequenza lineare in una macchina a stati con un dispatcher, il che rende inutile leggere il codice dall'alto in basso.
Quanto costa: il conto vero#
# Il conto va fatto, non stimato. Su questo sito:npm run build📊 Dimensione bundle: 17.42 KB (bundle + minifica)📊 Dimensione offuscato: 148.05 KB (dopo l'offuscamento)📊 Dimensione CSS: 49.43 KB → 32.21 KB# Cioe': l'offuscamento moltiplica per ~8.5 il JavaScript.# In trasmissione la compressione recupera molto...gzip -c public/js-build/app.js | wc -c38214# ...ma il browser deve comunque DECOMPRIMERE, INTERPRETARE ed# ESEGUIRE 148 KB, non 38. Il costo di esecuzione non si comprime.| Minificazione | Offuscamento | |
|---|---|---|
| Obiettivo | prestazioni | deterrenza |
| Effetto sui byte | −60/70% | +700/800% |
| Effetto sull'esecuzione | neutro o positivo | negativo (flusso appiattito) |
| Rischio di rompere qualcosa | praticamente nullo | reale: selfDefending, debugProtection |
| Stack trace in produzione | gestibili con source map private | illeggibili |
| Impedisce la copia? | no, né ci prova | no: la rende costosa |
| Quando | sempre | quando la deterrenza vale il prezzo |
Le tre domande prima di attivarlo#
// La domanda da porsi prima di attivarlo, in tre punti://// 1. Che cosa sto proteggendo?// Se la risposta è "la logica di business" → quella non dovrebbe// stare nel browser. Va spostata sul server, e il problema sparisce.// Se è "l'implementazione di un'interfaccia curata" → l'offuscamento// ha senso come deterrente contro la copia opportunistica.//// 2. Da chi?// Da un curioso che apre DevTools → sì, funziona benissimo.// Da un concorrente determinato → alza il costo di qualche giorno.// Non lo impedisce, e va detto onestamente.//// 3. A che prezzo?// Byte, tempo di esecuzione, e stack trace di produzione illeggibili// — cioè la capacità di capire perché qualcosa si rompe sui browser// degli utenti. Quest'ultima è la voce che si sottovaluta di più.La prima domanda è quella che risolve più casi. Se ciò che si vuole proteggere è la logica di business — un algoritmo di calcolo, una regola di prezzo, un metodo che vi distingue — quella logica non dovrebbe trovarsi nel browser, offuscata o meno. Spostarla su un endpoint del server elimina il problema invece di mitigarlo, e come discusso in che cosa si può proteggere nel frontend, è l'unica misura che regge davvero.
Resta il caso legittimo: un frontend curato, con lavoro di interfaccia e di interazione che si vuole non venga copiato di peso. Lì l'offuscamento fa il suo mestiere di attrito — scoraggia la copia opportunistica, che è la stragrande maggioranza — e il prezzo in byte è una scelta consapevole. È la scelta fatta su questo sito, con il conto sotto gli occhi.
L'ultimo anello: la compressione#
Non è una fase di build ma una configurazione del server, e va verificata perché quando manca nessuno se ne accorge — il sito funziona, semplicemente pesa due o tre volte tanto. Su Caddy è una direttiva encode zstd gzip; su nginx un blocco gzip on con l'elenco dei tipi MIME.
Il controllo va fatto chiedendo esplicitamente la codifica, altrimenti curl riceve la versione non compressa e sembra che non ci sia:
curl -sI -H 'Accept-Encoding: zstd, gzip, br' https://esempio.it/js-build/app.js \> | grep -i 'content-encoding'content-encoding: zstd