No. 100

MicroscopioTitolo originale: Better JavaScript Minification

Pubblicato in: Javascript, Workflow & Tools

Scritto da Nicholas C. Zakas

Negli scorsi anni, si è diffusa enormemente la ricerca sulla performance, grazie agli studi dello Yahoo! Exceptional Performance Team e di Steve Souders di Google. La maggior parte di questa ricerca studia non la singola pagina HTML, bensì le risorse necessarie a quella pagina per essere visualizzata e per avere il comportamento previsto.

Sebbene sia i CSS sia JavaScript possano essere inclusi in una pagina HTML, le best practice incoraggiano la memorizzazione del codice CSS e JavaScript in file esterni, che possano essere scaricati e memorizzati separatamente. La ricerca sulla performance ha come fulcro il seguente interrogativo: come si possono scaricare ed applicare queste risorse esterne in maniera più efficiente? Il primo approccio consiste nel limitare il numero delle richieste esterne dal momento che l'overhead di ogni richiesta HTTP è alto. Il secondo approccio? Fate in modo che il vostro codice sia quanto più piccolo possibile.

La storia del risparmio dei byte di JavaScript

Douglas Crockford introdusse JSMin nel 2004 come un modo per rimpicciolire i file JavaScript prima che vengano messi in un ambiente di produzione. Il suo semplice tool rimuoveva gli spazi, i tab ed i commenti dai file JavaScript, riducendone sostanzialmente la dimensione rispetto al file originale. La logica dietro al suo tool era valida: ridurre la dimensione del codice JavaScript voleva dire aumentarne la velocità di download, risultando in una migliore esperienza d'uso.

Tre anni più tardi, l'ingegnere di Yahoo! Julien Lecomte presentò lo YUI Compressor. Lo scopo dello YUI Compressor consisteva nel ridurre la dimensione dei file JavaScript ancora di più che con JSMin, applicando delle ottimizzazioni accorte al codice sorgente: oltre e rimuovere i commenti, gli spazi e i tabe, lo YUI Compressor rimuove in maniera sicura i line break, diminuendo la dimensione complessiva del file. Tuttavia, il maggior risparmio sui byte lo si ottiene rimpiazzando i nomi delle variabili locali con nomi composti da uno o due caratteri. Ad esempio, supponete di avere la seguente funzione:

  function sum(num1, num2) {
return num1 + num2;
}

Lo YUI Compressor la fa diventare così:

function sum(A,B){return A+B;}

Notate come le due variabili locali, num1 and num2, siano state rimpiazzate da A e B, rispettivamente. Dal momento che lo YUI Compressor effettivamente fa il parsing dell'intero input di JavaScript, può sostituire i nomi delle variabili locali in maniera sicura, senza introdurre errori nel codice. La funzione nel complesso continua a funzionare come in origine dal momento che i nomi delle variabili sono irrilevanti per la sua funzionalità. In media, lo YUI Compressor può comprimere i file fino al 18% in più rispetto a JSMin.

Al giorno d'oggi, la pratica comune è quella di usare un tool di riduzione della dimensione insieme alla compressione HTTP, per ridurre ancora di più la dimensione dei file JavaScript. Tutto ciò risulta in un maggior risparmio rispetto all'uso di un singolo metodo.

Aumentare la minimizzazione

Un paio di anni fa, quando ho cominciato a fare il debug di grandi quantità di codice di produzione, ho realizzato che lo YUI Compressor non applicava la sostituzione del nome della variabile ad una porzione piuttosto significativa del mio codice. Infastidito da quello che consideravo un grosso spreco di bytes, ho esplorato i pattern di codifica dello YUI Compressor per aumentarne i poteri di riduzione. Ho presentato i miei risultati, Extreme JavaScript Compression with YUI Compressor [JavaScript: compressione estrema con lo YUI Compressor, ndr], internamente a Yahoo!.

Durante le mie indagini, ho scoperto dei coding pattern che impedivano allo YUI Compressor di fare la sostituzione del nome della variabile: modificando o evitando questi coding pattern, si può migliorare la performance dello YUI Compressor.

Le feature diaboliche di JavaScript

Tutti coloro i quali hanno seguito le lezioni o letto gli scritti di Douglas Crockford, sanno a cosa ci si riferisce quando si parla delle parti “diaboliche” di JavaScript: si tratta di quelle parti che disorientano e/o ci impediscono di scrivere codice pulito che abbia una buona performance. La funzione eval() e l'istruzione with sono due colossali esempi di JavaScript diabolico. Sebbene ci siano altri fattori, entrambe queste feature forzano lo YUI Compressor a smettere di sostituire variabili. Per capire perché, dobbiamo capire quanto sia intricato il loro funzionamento.

Lavorare con eval()

Il compito dell'istruzione eval() è quello di prendere una stringa ed interpretarla come codice JavaScript. Ad esempio:

eval("alert('Hello world!');");

La parte infida di eval() è che ha accesso a tutte le variabili e funzioni che le stanno intorno. Ecco un esempio più complesso:

  var message = "Hello world!";

function doSomething() {
eval("alert(message)");
}

Quando chiamate doSomething(), viene visualizzato un avviso insieme al messaggio “Hello world!”. Questo accade perché la stringa passata ad eval() accede alla variabile globale message e lo visualizza. Ora, considerate cosa succederebbe se rimpiazzaste automaticamente il nome della variabile message:

  var A = "Hello world!";

function doSomething() {
eval("alert(message)");
}

Avrete notato come cambiare il nome della variabile in A risulti in un errore quando doSomething() va in esecuzione (dal momento che message non è definita). Il primo compito dello YUI Compressor è quello di preservare la funzionalità del vostro script, quindi quando trova eval() smette di rimpiazzare le variabili. Questa potrebbe non sembrare un'idea così cattiva finché non realizzate tutte le implicazioni: la sostituzione dei nomi di variabile è impedita non solo nel contesto locale in cui eval() è chiamata, ma in tutti i contesti che le stanno attorno. Nell'esempio precedente, ciò significa che sia il contesto all'interno di doSomething() che il contesto globale non possono avere i nomi di variabile sostituiti.

Usare eval() ovunque nel codice implica che i nomi delle variabili globali non saranno mai cambiati. Considerate l'esempio seguente:

  function handleJSONP(object) {
return object;
}

function interpretJSONP(code) {
var data = eval(code);

//process data
}

In questo codice, fate finta che handleJSONP() e interpretJSONP() siano definite nel mezzo di altre funzioni. JSONP è un formato di comunicazione Ajax molto usato che richiede che la risposta sia interpretata dal motore di JavaScript. Per questo esempio, un modello di risposta JSONP potrebbe essere come questo:

  handleJSONP({message:"Hello world!"});

Se riceveste indietro questo codice dal server attraverso una chiamata XMLHttpRequest, lo step seguente sarebbe quello di valutarlo: a questo punto eval() diventa molto utile. Ma il solo fatto di avere eval() nel codice significa che nessuno dei nomi degli identificatori globali potrà essere sostituito. L'opzione migliore consiste nel limitare il numero di variabili globali che introducete.

Potete spesso farla franca creando una funzione anonima che si esegua da sé, tipo:

  (function() {
function handleJSONP(object) {
return object;
}

function interpretJSONP(code) {
var data = eval(code);

//process data
}
})();

Questo codice non introduce alcuna nuova variabile globale, ma dal momento che viene usata eval(), nessun nome di variabile verrà rimpiazzato. Il risultato definitivo (110 bytes) è il seguente:

(Gli a capo sono segnati con » —Ed.)

  (function(){function handleJSONP(object){return object}function »
interpretJSONP(code){var data=eval(code)}})();

La cosa carina di JSONP è che si basa sull'esistenza di un unico identificatore globale: la funzione a cui deve essere passato il risultato (in questo caso, handleJSONP()). Questo significa che non ha bisogno di accedere ad alcuna variabile locale o funzione e vi dà l'opportunità di segregare la funzione eval() nella sua funzione globale. Notate inoltre che dovete spostare handleJSONP() al di fuori perché sia globale, così che nemmeno il suo nome venga sostituito:

  //my own eval
function myEval(code) {
return eval(code);
}

function handleJSONP(object) {
return object;
}

(function() {
function interpretJSONP(code) {
var data = myEval(code);

//process data
}
})();

La funzione myEval() ora funziona come eval() tranne che non può accedere alle variabili locali. Può comunque accedere a tutte le variabili globali e alle funzioni. Se il codice che viene eseguito da eval() non avrà mai bisogno di accedere alle variabili locali, allora questo approccio è il migliore. Tenendo l'unico riferimento a eval() al di fuori della funzione anonima, farete in modo che ogni nome di variabile all'interno di quella funzione possa essere sostituito. Ecco l'output:

  function myEval(code){return eval(code)}function handleJSONP »
(a){return a}(function(){function a(b){var c=myEval(b)}})();

Potete vedere che sia interpretJSON(), code e data sono stati rimpiazzati (da a, b e c rispettivamente). Il risultato è di 120 bytes, che, noterete, è maggiore dell'esempio senza eval() segregato. Questo non vuol dire che l'approccio è bacato, è solo che questo codice d'esempio è fin troppo piccolo per vederne l'impatto. Se applicaste questo cambiamento su un codice JavaScript di 100KB, allora vedreste che il codice risultante è molto più piccolo rispetto a lasciare eval() al suo posto.

Ovviamente, la miglior opzione è quella di non usare eval() per niente, poiché facendo ciò vi evitereste molte acrobazie per rendere felice lo YUI Compressor. Comunque, se proprio dovete, la vostra scelta migliore per una minimizzazione ottimale è quella di segregare eval().

L'istruzione with

L'istruzione with è la seconda feature diabolica che interferisce con la tecnica di sostituzione delle variabili dello YUI Compressor. Per quelli che non vi hanno familiarità, l'istruzione with è stata progettata (in teoria) per ridurre la dimensione del codice eliminando il bisogno di scrivere gli stessi nomi di variabile più volte. Considerate il seguente esempio:

  var object = {
message: "Hello, ",
messageSuffix: ", and welcome."
};
object.message += "world" + object.messageSuffix;
alert(object.message);

L'istruzione with vi permette di riscrivere il codice come:

  var object = {
message: "Hello, ",
messageSuffix: ", and welcome."
};
with (object) {
message += "world" + messageSuffix;
alert(message);
}

Effettivamente, l'istruzione with evita il bisogno di ripetere “object” più volte all'interno del codice. Ma questo risparmio ha un suo costo. Innanzitutto, ci sono delle implicazioni sulla performance usando l'istruzione with, poiché si accede più lentamente alle variabili locali. Ciò accade perché le variabili all'interno dell'istruzione with sono ambigue fino al momento dell'esecuzione: potrebbero essere di proprietà del oggetto del contesto dell'istruzione with oppure essere variabili della funzione o un altro contesto di esecuzione. Per meglio comprendere questa ambiguità, osservate il codice quando la variabile locale message viene aggiunta e viene rimossa la definizione di object:

  var message = "Yo, ";

with (object) {
message += "world" + messageSuffix;
alert(message);
}

Quando l'identificatore message è usato all'interno dell'istruzione with, potrebbe far riferimento alla variabile locale message oppure potrebbe far riferimento ad una proprietà chiamata message su object. Dal momento che JavaScript è un linguaggio “late binding”, non ci sono modi per sapere quale sia il vero riferimento per message senza aver prima eseguito il codice ed aver determinato se object ha la proprietà chamata message. Osservate come il late binding ha effetto su questo codice:

  function displayMessage(object) {
var message = "Yo, ";

with (object){
message += "world" + messageSuffix;
alert(message);
}
}

displayMessage({ message: "Hello, ", messageSuffix: ", and welcome." });
displayMessage({ messageSuffix: ", and welcome." });

La prima volta che viene chiamato displayMessage(), l'oggetto che gli viene passato ha la proprietà chiamata message. Quando va in esecuzione l'istruzione with, il riferimento a message viene mappato sulla proprietà dell'oggetto e pertanto il messaggio visualizzato è “Hello, world, and welcome.”. La seconda volta, l'oggetto che viene passato ha solo la proprietà messageSuffix, col significato che il riferimento a message all'interno dell'istruzione with fa riferimento alla variabile locale; il messaggio visualizzato è quindi “Yo, world, and welcome.”.

Dal momento che lo YUI Compressor in effetti non esegue il codice JavaScript, non ha possibilità di sapere se gli identificatori in un'istruzione with siano proprietà dell'oggetto (nel qual caso, non è sicuro rimpiazzarli) o se siano riferimenti a variabili locali (in questo caso, è sicuro rimpiazzarli). Lo YUI Compressor tratta l'istruzione with allo stesso modo di eval(): quando è presente: non effettua la sostituzione delle variabili nella funzione né in nessuno dei contesti di esecuzione.

A differenza della funzione eval(), non c'è modo di segregare l'istruzione with in maniera che non abbia effetto sulla maggior parte del codice. La mia raccomandazione è di evitare del tutto di usare l'istruzione with: sebbene sembra che faccia risparmiare byte nel momento in cui scriviamo il codice, in realtà si perdono bytes rinunciando alla feature della sostituzione di variabile dello YUI Compressor. La funzione displayMessage() viene ridotta così:

  function displayMessage(object){var message="Yo, ";with(object) »
{message+="world"+messageSuffix;alert(message)}};

Si tratta di 112 bytes. Se si riscrive la funzione evitando l'istruzione with, displayMessage() diventa così:

  function displayMessage(object) {
var message = "Yo, ";

object.message += "world" + object.messageSuffix;
alert(object.message);
}

Quando viene ridotta, questa nuova versione della funzione diventa:

  function displayMessage(a){var b="Yo, ";a.message+="world"+ »
a.messageSuffix;alert(a.message)};

La dimensione di questa è di 93 bytes, nonostante il codice sorgente originale sia più grande. Il codice sorgente ridotto diventa più piccolo perché abbiamo usato la sostituzione di variabile.

Conclusioni

La funzionalità di sostituzione di variabile dello YUI Compressor può far risparmiare molti byte riducendo il vostro JavaScript. Dal momento che lo YUI Compressor cerca di evitare di danneggiare il vostro codice sostituendo in maniera errata dei nomi di variabile, non effettuerà la sostituzione di variabile quando vengono usati la funzione eval() o l'istruzione with. Queste feature “diaboliche” alterano la maniera in cui il codice JavaScript viene interpretato e impediscono allo YUI Compressor di sostituire in maniera sicura i nomi di variabile, al costo di molti byte non risparmiati. Evitate questa penalizzazione stando alla larga da eval() o segregandola lontano dal resto del vostro codice. Inoltre, evitate anche l'istruzione with. Questi step vi assicureranno che il vostro codice non ponga ostacoli ad una riduzione ottimale.

Share/Save/Bookmark
 

Discutiamone

Ti sembra interessante? Scrivi tu il primo commento


Cenni sull'autore

Nicholas C. Zakas

Foto di Nicholas C. ZakasNicholas C. Zakas (@slicknet) è il principal front-end engineer per la homepage di Yahoo! e contribuisce alla libreria Yahoo! User Interface (YUI). Ha scritto la Cookie Utility, il Profiler, e lo YUI Test. È l'autore di due libri: Professional JavaScript for Web Developers e High Performance JavaScript, è co-autore di Professional Ajax, e ha contribuito a Even Faster Web Sites. Nicholas scrive regolarmente sul suo blog riguardo il web development.