Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Javascript: ottimizzare le performance di caricamento

Tecniche e consigli per migliorare la velocità di caricamento dei nostri script
Tecniche e consigli per migliorare la velocità di caricamento dei nostri script
Link copiato negli appunti

I fattori che concorrono al successo di un sito web sono molteplici e spaziano dall'efficacia della comunicazione alla qualità dei contenuti. Sotto il punto di vista tecnico uno degli aspetti principali da tenere in considerazione è la velocità di caricamento delle pagine. Una scarsa performance in questo campo può essere legata a molteplici cause: connessioni poco affidabili, server sovraccarichi, immagini non ottimizzate o troppo numerose.

Da quando JavaScript è diventato onnipresente nei siti web, l'attenzione degli sviluppatori si è rivolta alla gestione e ottimizzazione degli script, anche considerando che spesso sono uno dei principali fattori di stress per il browser.

Il problema

Il problema principale nel caricamento di script JavaScript risiede nel modo in cui i browser stessi li caricano. Nei browser più recenti, le risorse richiamate attraverso un tag script sono scaricate in parallelo come tutte le altre (CSS, immagini, etc). Tuttavia esse vengono interpretate ed eseguite singolarmente in ordine di caricamento mentre l'interpretazione ed il rendering di altre risorse viene bloccato. Questo scenario si rivela ancora peggiore in browser come Internet Explorer 6 e 7, dove gli script vengono addirittura scaricati singolarmente bloccando il download di tutte le altre risorse.

Nelle intenzioni degli sviluppatori, questo comportamento serve a garantire che eventuali dipendenze fra gli script vengano rispettate e che funzioni da lanciare al caricamento del DOM siano già disponibili. In pratica, nel caricamento di due script da 50KB e 3KB il browser attenderà che il primo sia scaricato ed interpretato lasciando in attesa il secondo.

Ottimizzazioni di base

Dato questo problema, le soluzioni più comunemente diffuse intervengono in tre direzioni:

  • riducendo il peso dei file attraverso la minificazione e compressione del codice. YUI Compressor, Closure Compiler, e Packer sono fra i tool più affidabili ed efficaci in questa operazione.
  • diminuendo il numero di file da scaricare per mezzo della concatenazione degli script.
  • posizionando i tag script prima della chiusura del body invece che nell'head, in modo che il motore di rendering li incontri solamente dopo aver scaricato risorse fondamentali al rendering della pagina come HTML e CSS, ma sempre prima del caricamento completo del DOM.

Nonostante esistano versioni online degli strumenti descritti in precedenza, è anche disponibile un plugin per Eclipse chiamato Web Optimizer di Rockstarapps che integra con facilità compressione ed ottimizzazione direttamente nel vostro progetto.

Svantaggi

Sebbene concatenazione e minificazione siano considerate pratiche positive in fase di pubblicazione di un progetto, è importante sottolinearne alcuni effetti collaterali.

Anzitutto la concatenazione comporta la generazione di file di grosse dimensioni i quali, essendo scaricati da un unico processo, possono aumentare i tempi di download diminuendo paradossalmente le performance. Inoltre la concatenazione e la minificazione risultano efficaci se applicate a script di grandi dimensioni e comuni all'intero progetto (per esempio le librerie), mentre sarebbe buona regola non includere script usati solo in poche, determinate pagine (come Lightbox o validatori client-side) per evitare che vengano caricati inutilmente.

Ottimizzazioni avanzate

La regola che si deduce dai paragrafi precedenti è che, poiché ogni progetto ha caratteristiche particolari, seguire ciecamente le regole base di ottimizzazione non sempre significa raggiungere i risultati migliori.

Esistono diverse tecniche che opportunamente mixate possono darci buoni risultati in ogni situazione, fra queste la più interessante è il caricamento asincrono degli script per aggirare la natura bloccante del tag script.

Sebbene le ultime versioni dei browser mettano a disposizione strumenti come i Web Workers, il loro limitato supporto (nullo in IE) ha obbligato gli sviluppatori ad implementare soluzioni alternative. Oltre a questo va sottolineato che i Web Workers, proprio per la loro natura di processi in background, non possono interagire con il DOM del documento necessitando di operazioni ulteriori.

Caricamento dinamico

Al giorno d'oggi una tecnica collaudata per bypassare la natura bloccante del tag script è quella di iniettare dinamicamente gli script all'interno del documento. Una semplice implementazione di questa strategia è la seguente:

function addScript (src) {
    var tag = document.createElement('script');
    tag.src = src;
    tag.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(tag);
}

Come è facile intuire, perdendo la propria natura bloccante gli script caricati in questo modo non verranno processati in ordine, quindi eventuali dipendenze potrebbero non essere rispettate.

La soluzione migliore, allora, è affidarsi a librerie che implementino un sistema di dipendenze. Nella famiglia dei framework troviamo un sistema di dipendenze in Dojo, YUI e Google Closure, tuttavia inizialmente prenderemo in considerazione due soluzioni standalone: LABjs e RequireJS.

LABjs

LABjs è un acronimo per Loading And Blocking JavaScript ed è sviluppata da Kyle Simpson in associazione con Steve Souders.

A differenza di altre soluzioni che permettono di definire interi moduli da caricare, LABjs si concentra sulla semplicità di utilizzo e su una curva di apprendimento pressoché nulla.

Prendiamo ad esempio il seguente codice nel quale viene caricata una libreria (dipendenza) è due script:

<script src="libreria.js" type="text/javascript"></script>
<script src="script1.js" type="text/javascript"></script>
<script src="script2.js" type="text/javascript"></script>
<script type="text/javascript">
    script1Init();
    script2Init();
</script>

Con LABjs il codice verrà riscritto in questo modo:

<script src="lab.js" type="text/javascript"></script>
<script type="text/javascript">
    $LAB
    .script('libreria.js').wait()
    .script('script1.js')
    .script('script1.js')
    .wait(function () {
        script1Init();
        script2Init();
    });
</script>

In questo esempio tutti gli script vengono caricati in parallelo, ma libreria.js viene interpretata per prima, quindi vengono interpretati gli altri script (senza un ordine preciso). Alla fine vengono lanciate le due funzioni di init.

Una particolarità del metodo .script() è quella di accettare come argomento anche un array di stringhe il che, unito alla possibilità di forzare il sitema di dipendenze per tutti gli script nella coda, rende molto semplice il caricamento di moduli JavaScript dipendenti:

var stack = ['libreria.js', 'plugin.libreria.js', 'main.js'];
$LAB
.setOption({AlwaysPreserveOrder : true}) //preserva l'ordine di interpretazione
.script(stack);

Per chi volesse approfondire la conoscenza di questa libreria, ne è disponibile una dettagliata documentazione sul sito web, comprendente una serie di accorgimenti e suggerimenti per evitare problemi e ottimizzare al meglio il sistema, nonché una test suite per verificare l'effettivo aumento delle performance.

RequireJS

Pur mantenendo le stesse caratteristiche e finalità di LABjs, RequireJS di James Burke permette di gestire in modo ordinato applicazioni più complesse con numerose dipendenze. L'implementazione più semplice di RequireJS prevende il caricamento di file JavaScript esterni e l'esecuzione di una funzione di callback alla fine dell'interpretazione:

<script src="require.js" type="text/javascript"></script>
<script type="text/javascript">
    require(['libreria.js', 'script1.js'], function() {
        script1Init();
    });
</script>

Oltre a ciò RequireJS introduce il concetto di modulo, cioè di un oggetto in cui definire sia lo script da richiamare che eventuali dipendenze, proprietà e metodi. Ecco un esempio completo delle potenzialità offerte dalla libreria:

//definisco un nuovo modulo per la gestione
//dell'invio di email
require.def('email,
    //carico altri due moduli dipendenti
    ['form', 'modal'],
    //quindi restituisco un oggetto
    //che definisca il modulo con proprietà e metodi
    function(form, modal) {
        //form e modal sono i due oggetti definiti nei moduli
        //caricati in precedenza 
        return {
            proprieta1 : 1,
            proprieta2 : false,
            invia: function() {
                if (form.valido(this) && form.invioDati(this)) {
                    modal.success('E-mail inviata!');
                } else {
                    modal.error('Errore!');
                }
            }
        }
    }
);

A questo punto per usare il modulo basterà scrivere il codice seguente:

<script src="require.js" type="text/javascript"></script>
<script type="text/javascript">
    require('email', function(email) {
        //esegui solo on DOM ready
        require.ready(function () {
            email.invia();
        });
    });
</script>

Per chi fosse interessato ad approfondire la conoscenza di RequireJS, è disponibile una documentazione esaustiva dove vengono spiegate anche funzionalità avanzate relative alla configurazione, ai moduli di localizzazione linguistica e alla gestione di versioni multiple dei moduli.

Implementazioni nei framework

Poiché il tema delle performance e della gestione modulare delle librerie è molto sentito, alcuni framework hanno implementato un sistema di caricamento interno simile a quello di LABjs e RequireJS. Fra questi troviamo Dojo, che offre il metodo dojo.require() per caricare moduli e dipendenze:

<script type="text/javascript" src="dojo.js"></script>
<script type="text/javascript">
    dojo.require('app.module');
    dojo.ready(function () {
        //codice da eseguire quando il modulo e
        //le dipendenze sono state caricate.
    });
</script>

Essendo fortemente strutturati a moduli, altri due framework che offrono sistemi simili a Dojo sono Google Closure con goog.require() e YUI con YUI.use(). La differenza principale fra le due librerie è che mentre Closure necessita di un caricamento esplicito di tutte le dipendenze, YUI carica automaticamente tutte le dipendenze di un modulo se non già disponibili. Ecco due esempi:

//moduli con Google Closure
goog.require('goog.dom');
goog.require('goog.dom.query');
var element = goog.dom.query('#mioDiv > a');
//moduli con YUI3
YUI.use('dd', function (Y) {
    /*
    il modulo 'dd' (drag and drop)
    richiama automaticamente le dipendenze
    come 'node'
    */
    var dd = new Y.DD.Drag({ ... });
    var element = Y.all('#mioDiv > a');
});

Per quanto riguarda jQuery, non esiste un metodo nativo per il caricamento dinamico degli script, tuttavia ci si può affidare a due soluzioni interessanti: NBL e Sexy.js.

Una volta collegata al documento con un tag script la prima libreria preleva l'ultima versione di jQuery dal CDN di Google, andando poi ad effettuare tutte le operazioni che avremo opportunamente impostato nell'attributo opt del tag stesso; ecco un esempio:

<script type="text/javascript" src="nbl.js" opt="{plugins: [ 'jquery.lightbox.min.js', 'jquery.carousel.min.js' ], ready: my_ready_function }">
</script>

In questo esempio, alla fine dei caricamento di jQuery, verranno caricati due plugin e quindi lanciata la funzione my_ready_function.

Sexy.js è una libreria disponibile sia standalone che come plugin jQuery per chiamate AJAX sequenziali (SAJAX), ma può anche essere usata per caricare dinamicamente file JavaScript, CSS e JSON. La sintassi è molto simile a quella di LABjs, tuttavia va fatto notare che gli script locali vengono caricati attraverso una chiamata AJAX invece di essere iniettati nel DOM come tag script:

<script src="Sexy.js" type="text/javascript"></script>
<script type="text/javascript">
    Sexy
        .script('file1.js')
        .script('file2.js', function () {
        init();
        });
</script>

Risultati nei browser

Il risultato principale dell'adozione di questa tecnica, oltre ad una migliore gestione del caricamento e dell'interpretazione del codice, risiede nel fatto che l'eliminazione della fase bloccante nel caricamento della pagina rende minore il tempo di caricamento generale (in media 2, 3 volte inferiore) e soprattutto anticipa l'evento DOM ready associato molto spesso all'esecuzione di librerie e interazioni.

Interpretazione differita

In questi ultimi periodi, una delle tecniche che ha fatto molto parlare di sé è l'intepretazione differita del codice. L'idea di fondo è quella di scaricare il codice JavaScript come un normale file di testo oppure come parte testuale del documento, per differire in eventi specifici la sua interpretazione ed esecuzione, evitando in questo modo di bloccare il browser per logiche JavaScript non necessarie sul momento.

Allo stesso tempo un altro vantaggio di questa tecnica, soprattutto in ambito mobile, è quello di poter scaricare buona parte del codice JavaScript all'interno della pagina, risparmiando sul numero delle connessioni di rete senza tuttavia dover impegnare la CPU per l'interpretazione.

Le tecniche utilizzate per ottenere questo risultato sono varie e purtroppo non è possibile garantire risultati ottimali per tutti i dispositivi. In questo caso sarà necessario individuare un target principale ed agire di conseguenza.

Codice commentato

La prima ed ingegnosa tecnica di interpretazione (ed esecuzione) differita è stata sviluppata dai programmatori di Google per la versione mobile di Gmail e consiste nell'inserire il codice commentato all'interno di un tag script. Ecco un esempio:

<script type="text/javascript" id="mioScript">
    /*
    alert('Questo è un codice commentato')
    */
</script>
<script type="text/javascript">
    function eseguiCodice (codice) {
        codice = codice.substr(2,-2);
        eval(codice);
    }
    var mioLink = document.getElementById('mioLink');
    //questo codice sarà intepretato solo quando l'utente cliccherà
    //sul link #mioLink
    mioLink.onclick = function () {
        eseguiCodice( document.getElementById('mioScript').text );
    };
</script>

Codice come stringa

La seconda tecnica di questo tipo è stata sviluppata da Charles Jolley di SproutCore e consiste nello scaricare il codice sotto forma di stringa di testo, per poi interpretarlo ed eseguirlo tramite la funzione eval():

<script type="text/javascript">
var codice = "alert('Questo è un dentro una stringa')";
</script>
<script type="text/javascript">
    var mioLink = document.getElementById('mioLink');
    //questo codice sarà intepretato solo quando l'utente cliccherà
    //sul link #mioLink
    mioLink.onclick = function () {
        eval(codice);
    };
</script>

Script Islands

La tecnica delle isole di script è stata presentata da Michael Mahemoff e parte dalla constatazione che il codice inline contenuto in un tag script viene sempre scaricato durante il caricamento, ma viene interpretato ed eseguito solo se l'attributo type dello stesso è impostato su "text/javascript".
La tecnica proposta si sviluppa quindi nel seguente modo:

<script type="x-deferred-script" id="mioScript">
    //codice scaricato
    //ma non interpretato o eseguito
    alert('Questo è un codice in una script island')
</script>
<script type="text/javascript">
    var mioLink = document.getElementById('mioLink');
    //questo codice sarà intepretato solo quando l'utente cliccherà
    //sul link #mioLink
    mioLink.onclick = function () {
        eval( document.getElementById('mioScript').text );
    };
</script>

Codice in una closure

Le closure sono una tecnica molto potente a disposizione degli sviluppatori JavaScript e possono tornare utili anche per differire l'esecuzione del codice. Ecco un esempio:

<script type="text/javascript">
    //codice scaricato e valutato
    //ma non eseguito
    var factory = (function () {
        alert('Questo è un dentro una closure');
    });
</script>
<script type="text/javascript">
    //eseguo il codice in un secondo momento
    factory();
</script>

Risultati nei browser

La differenza principale fra queste tecniche è che le prime tre si limitano a scarica il codice, mentre l'ultima passa anche per la fase bloccante dell'interpretazione.

Nonostante il processo completo dal download all'esecuzione non risulti particolarmente più veloce rispetto alla tecnica tradizionale, va notato come il tempo di caricamento totale di una pagina sia notevolmente inferiore, garantendo quindi una migliore esperienza utente.

Un grafico con i risultati pratici (esclusa la tecnica delle script islands) è disponibile a questo indirizzo.

Il futuro

Oltre all'uso dei Web Workers, in futuro (HTML5) potremo contare anche su due nuovi attributi del tag script: async e defer. Il primo esegue il codice appena è disponibile (scaricato ed interpretato), mentre il secondo rimanda l'esecuzione fino al caricamento completo della pagina.

Al momento il supporto da parte dei browser è abbastanza limitato e solo IE interpreta correttamente defer (che in realtà era già parte di HTML4).


Ti consigliamo anche