Il problema prospettato dal titolo dell'articolo può essere sintetizzato in: minimizzare il tempo che intercorre tra la richiesta di una pagina da parte del visitatore e l'istante in cui tale pagina viene completamente disegnata dal suo browser. Purtroppo alla semplicità di enunciazione non corrisponde un'altrettanto semplice, immediata ed univoca soluzione.
I fattori che contribuiscono a determinare i tempi di risposta sono molti e variabili, basti pensare alle differenze tra un sito che fornisce solo pagine HTML statiche ed uno che fornisce pagine dinamiche, costruite interrogando un database. Nel secondo caso anche le caratteristiche del linguaggio lato server (PHP, ASP, etc.) e del DBMS (MySQL, SQL Server, etc.) sono elementi determinanti, per i quali saranno necessarie specifiche ottimizzazioni. La jungla si infittisce se introduciamo anche considerazioni sulle risorse hardware e la loro configurazione, sulla larghezza di banda e l'ottimizzazione del suo utilizzo.......solo ad elencarle viene il mal di testa!
Di seguito ci limiteremo, si fa per dire, ad analizzare il problema dal punto di vista della struttura di una pagina HTML. È chiaro che le considerazioni fatte avranno valenza generale, costituendo comunque un passo importante nel processo di ottimizzazione. Per nostra fortuna in rete è disponibile una ricca documentazione sull'argomento, in particolare sotto forma di raccolte di regole, le cosiddette best practices. Esse costituiscono una sorta di vademecum da seguire nello sviluppo web, sintetizzando per punti le azioni da intraprendere. Le più famose sono certamente le Web Performance Best Practices di Google e le Best Practices for Speeding Up Your Web Site di Yahoo.
Lo scopo primario di questo articolo è preparare il terreno per la descrizione di un modulo open source per Apache 2: mod_pagespeed . Poiché tale modulo automatizza il processo di ottimizzazione basandosi sulle best practices di Google, queste ultime costituiranno il nostro principale riferimento. In particolare quelle sezioni che più si caratterizzano come azioni "lato server", anche se la linea di demarcazione non è sempre netta data la stretta collaborazione tra amministratore di sistema e programmatore web in quest'ambito.
Sfruttiamo la Cache
Come sappiamo una pagina web moderna è composta, oltre che da codice HTML, da un insieme di altre risorse: immagini, fogli di stile, script, file multimediali ed altro. Ciascuna di esse richiede un certo tempo per il download andando ad incrementare il tempo complessivo di caricamento della pagina. Molti componenti non sono soggetti a modifiche frequenti, per questo i browser possono salvarli localmente nella cosiddetta cache. Dopo la prima visita potranno così utilizzare la copia locale, anziché riprelevare la risorsa dal server. Ciò consente sia di ridurre il peso complessivo della pagina, sia il numero di richieste HTTP inviate al server, fattore molto importante come vedremo meglio più avanti. I principali browser moderni sono dotati di un sistema euristico per decidere quanto tempo mantenere localmente un elemento della pagina prima di considerarlo obsoleto e riscaricarlo.
Per ottenere risultati migliori dobbiamo però configurare il web server perché indichi esplicitamente al browser come comportarsi con una particolare risorsa o tipologie di risorse. La comunicazione può avvenire tramite opportune intestazioni o header HTTP. Essi costituiscono il silenzioso colloquio tra client e server in quanto sono dati effettivamente scambiati, ma non direttamente visualizzati dall'utente. Eccone un esempio, dove risultano evidenziati gli header più rilevanti per le nostre considerazioni:
Vary: Accept-Language,Cookie,Referer Content-Type: text/html; charset=UTF-8 Etag: "ddb0427dab362d9fe748aceb32decdf2" Last-Modified: Sat, 12 Nov 2011 16:42:05 GMT Date: Sat, 12 Nov 2011 16:42:05 GMT Expires: Sat, 12 Nov 2011 17:42:05 GMT Cache-Control: public, max-age=3600 X-Content-Type-Options: nosniff Content-Encoding: gzip Server: codesite_static_content Content-Length: 4870
Il protocollo HTTP/1.1 prevede due tipologie di intestazioni, Expires
e Cache-Control
, tra loro analoghe. La prima specifica una data assoluta di "scadenza" della risorsa, nell'esempio Sat, 12 Nov 2011 17:42:05 GMT. La seconda, tramite l'opzione max-age=<secondi>, fornisce invece il numero di secondi, a partire dal momento della richiesta, per cui la risorsa potrà considerarsi "fresca". Nell'esempio sono 3600 secondi, ovvero un'ora.
Vediamo come usarle con esempi pratici.
È importante che almeno una delle due sia impostata, entrambe sono di fatto ridondanti. Expires
viene considerata maggiormente supportata dai sistemi di caching, mentre Cache-Control
, con tutte le possibili opzioni, offre un controllo maggiore. Nel caso del web server Apache possiamo utilizzare le direttive del modulo mod_expires. Per impostare, ad esempio, la scadenza di un gruppo di file un mese dopo l'accesso:
<FilesMatch ".(ico|pdf|flv|jpe?g|png|gif|js|css|swf)$"> ExpiresActive On ExpiresDefault "access plus 1 month" </FilesMatch>
Invece di specificare le estensioni dei file possiamo anche agire sui mime type. Ad esempio, per impostare una scadenza basata sulla data di modifica delle immagini in formato GIF:
ExpiresByType image/gif "modification plus 6 hours 2 minutes"
Un altro modulo di Apache molto utile per agire sulle intestazioni HTTP è mod_headers:
<FilesMatch ".(ico|pdf|flv|jpe?g|png|gif|js|css|swf)$"> Header set Expires "Thu, 15 Apr 2010 20:00:00 GMT" </FilesMatch>
Vi sono altre due intestazioni, concettualmente simili, che ci permettono di influenzare i meccanismi di caching dei browser: Last-Modified
e ETag
. Le potete trovare in grassetto nell'esempio iniziale, ma le riporto per facilità di lettura:
Etag: "ddb0427dab362d9fe748aceb32decdf2" Last-Modified: Sat, 12 Nov 2011 16:42:05 GMT
Entrambe forniscono un sistema che permette al programma di navigazione di controllare se la risorsa salvata localmente sia identica o meno a quella disponibile sul server. La prima, Last-Modified, stabilisce l'ultima data di modifica della risorsa che quindi andrà riscaricata solo se anteriore a quella dichiarata dal server. Su questa possiamo intervenire così:
<FilesMatch ".(ico|pdf|flv|jpe?g|png|gif|js|css|swf)$"> Header set last-modified "Thu, 15 Nov 2011 20:00:00 GMT" </FilesMatch>
Etag
, Entity tag, è invece un valore univoco, generato dal server, che identifica un oggetto: se l'oggetto viene modificato cambia anche il suo etag. In questa situazione il client si limiterà ad inviare al web server delle semplici richieste condizionali If-Modified-Since e If-None-Match che, come è logico aspettarsi, avranno un tempo di latenza notevolmente inferiore rispetto alle richieste complete.
Attenzione però ad un problema legato ad ETag: il valore viene costruito sulla base di alcune caratteristiche del file legate anche all'host su cui è memorizzato. Apache sfrutta l'inode del file, la dimensione e la data di ultima modifica. Nel caso si utilizzino più server per gestire il proprio sito, se il browser preleva la risorsa da uno e poi tenta di validarla su un secondo, ovviamente la verifica fallirà e sarà costretto a riscaricarla. In questo caso otterremo un peggioramento delle prestazioni anzichè un miglioramento, cui si può ovviare disabilitando ETag con la direttiva:
FileETag none
Oppure costruendo un valore che sia il medesimo per tutti i server, ad esempio utilizzando solo la data di modifica e la dimensione del file:
FileETag -INode MTime Size
Il consiglio, in sintesi, è agire sugli header che regolano il caching delle risorse statiche, il che vuol dire non limitarsi alle classiche immagini, ma includere anche fogli di stile, script, file swf e via discorrendo. Non influenzeremo le prestazioni del sito la prima volta che il navigatore visualizzerà le nostre pagine, ma successivamente ridurremo il tempo di risposta anche del 50% o più. Eseguite sempre dei test e valutate anche eventuali controindicazioni delle soluzioni adottate. Un'ultima notazione: da RFC non si dovrebbe impostare oltre l'anno la data di scadenza di una risorsa.
La cache dei proxy server
Il proxy caching è un altro ambito in cui, influenzare il meccanismo di memorizzazione locale delle pagine, può portare i suoi frutti. Un web proxy server, come ad esempio Squid, è un programma che agisce in maniera analoga al browser del visitatore, ma crea una cache pubblica cui più navigatori possono accedere. Solo quando il primo utente richiederà una pagina questa verrà completamente scaricata dal proxy, di seguito esso soddisferà le richieste dei navigatori per le medesime risorse prelevandole dalla propria cache.
Molte aziende ed anche ISP utilizzano questo sistema che si interpone, spesso in maniera trasparente, tra i propri utenti ed i siti web visitati. Risulta immediato arguire come ciò permetta un risparmio di banda e riduca i tempi di risposta, dato che i contenuti risultano più "prossimi" al navigatore. Agire correttamente sugli header HTTP ci consente di sfruttare questo meccanismo anche a nostro vantaggio.
Perchè un proxy server si senta autorizzato a salvare le nostre pagine deve ricevere un header specifico:
Cache-control: public
Per ragioni di sicurezza in alcune circostanze è preferibile non sfruttare il proxy caching, in quanto memoria ad accesso pubblico. Si pensi ad esempio a pagine web che utilizzano i cookie o cifrate tramite il protocollo SSL. I proxy server in questi casi dovrebbero rifiutarsi di memorizzare alcunchè, anche in presenza di un'intestazione Cache control: public. A mio parere è meglio non rischiare e fornire un esplicito Cache control: private per limitare la memorizzazione al browser dell'utente.
Le richieste HTTP
Tutto ciò che abbiamo sin qui descritto avrà effetto a partire dalle successive visite del nostro sito. È importante però presentarsi bene anche al primo incontro con il visitatore, se questi avrà un'esperienza di navigazione positiva è più probabile che torni sulle nostre pagine.
In questo caso un possibile approccio per velocizzare la navigazione consiste nel ridurre il numero di richieste HTTP che il client rivolgerà al server. Una buona strategia di ottimizzazione può consistere non solo nel diminuirle, ma anche nel renderle parallele, in modo da ridurre la combinazione dei tempi derivante dal loro accodamento.
Semplificare la struttura delle pagine è sicuramente una buona idea, ma per farlo dobbiamo rinunciare ad offrire contenuti ricchi ed esteticamente gradevoli? Non necessariamente, dobbiamo solo trovare la maniera ottimale di fornirli ai browser.
Possiamo farlo, ad esempio, raggruppando i fogli di stile esterni in uno o due file al massimo ed allo stesso modo agire con i JavaScript, oppure utilizzando la tecnica dei CSS sprite per le immagini. Anche la collocazione degli elementi all'interno della pagina può essere determinante. Infatti poiché il codice JavaScript può modificare una pagina web, i browser solitamente attendono che lo script sia scaricato ed interpretato ritardando il rendering dei contenuti che lo seguono. In alcuni casi viene bloccato il download di risorse referenziate subito dopo lo script, determinando un vero e proprio accodamento di richieste....ma qui stiamo sconfinando nell'ambito del programmatore, anche se in verità l'utilizzo di mod_pagespeed richiede questo tipo di conoscenze.
L'amministratore di sistema può comunque avere il suo ruolo nell'ottimizzazione delle richieste HTTP. Le specifiche del protocollo HTTP/1.1, infatti, suggeriscono il download parallelo di massimo due risorse per hostname. Sebbene, per nostra fortuna, i browser non le seguano alla lettera è un dato di fatto che esista un limite in questo senso. Mettendo mano alla configurazione dei DNS possiamo però ingannare i browser. Creando dei record CNAME moltiplichiamo i domini corrispondenti al nostro server: www.miodominio.it, server1.midominio.it, server2.miodominio.it. Mediante Apache definiamo poi i corrispondenti host virtuali e suddividiamo tra essi le risorse, che potranno infine essere downlodate in parallelo.
Non è tutto rose e fiori, infatti troppi download paralleli possono mettere in crisi la CPU o saturare la banda disponibile, ottenendo l'effetto opposto. Inoltre un incremento degli hostname richiede un proporzionale aumento del numero di richieste DNS da parte del client, che tendono a dilatare i tempi risposta. Comunemente si considera che il giusto equilibrio sia compreso tra 2 e 4 domini.
Per testare i tempi di risposta riducendo le richieste o aumentando la parallelizzazione Firebug risulta uno strumento insostituibile con la sua sezione Net.
La compressione dei dati
I browser moderni supportano la ricezione di file css, javascript e HTML in formato compresso. Secondo le specifiche del protocollo HTTP/1.1, il client comunica al server tale sua peculiarità con un apposito header:
Accept-Encoding: gzip, deflate
Il web server, a questo punto, può comprimere la risposta utilizzando uno dei metodi elencati dal client, avvertendolo sempre mediante intestazione HTTP:
Content-Encoding: gzip
I metodi utilizzabili sono sostanzialmente due gzip o deflate. L'algoritmo per la compressione dei dati è il medesimo, ma il primo metodo risulta maggiormente supportato dai browser.
Il nostro web server di riferimento, Apache, si avvale di un apposito modulo per queste operazioni. Nelle vecchie versioni 1.3.x era mod_gzip, mentre nelle più recenti versioni 2.x prende il nome di mod_deflate.
Se vogliamo abilitare la compressione in uno specifico contenitore, possiamo utilizzare la direttiva SetOutputFilter
:
<Directory "/percorso_mia_directory"> SetOutputFilter DEFLATE </Directory>
Più spesso ci si troverà ad abilitare la compressione solo per alcuni tipi di file, in particolare basandosi sui mime type:
<Directory "/percorso_mia_directory"> AddOutputFilterByType DEFLATE text/html text/css text/javascript </Directory>
Possiamo anche ragionare al contrario, ovvero applicare la direttiva di compressione per tutti i file ed escluderne esplicitamente alcuni, come immagini, file pdf, formati di file già compressi:
<Directory "/percorso_mia_directory"> SetOutputFilter DEFLATE SetEnvIfNoCase Request_URI .(?:gif|jpe?g|png)$ no-gzip dont-vary SetEnvIfNoCase Request_URI .(?:exe|t?gz|zip|bz2|sit|rar)$ no-gzip dont-vary SetEnvIfNoCase Request_URI .pdf$ no-gzip dont-vary </Directory>
Risulta utile la direttiva BrowserMatch
per disabilitare la compressione, no-gzip, per quei browser che non la supportano o lo fanno in maniera non corretta. Di seguito una tipica configurazione per evitare tali problemi:
BrowserMatch ^Mozilla/4 gzip-only-text/html BrowserMatch ^Mozilla/4.0[678] no-gzip BrowserMatch bMSIE !no-gzip !gzip-only-text/html
Come si evince dall'esempio è possibile combinare le opzioni no-gzip e gzip-only-text/html per ottenere i migliori risultati. Se desiderate approfondire l'argomento potete trovare, sempre nella sezione server, un articolo completamente dedicato a mod_deflate.
Attenzione a non applicare in modo indiscriminato questa soluzione, infatti per risorse di piccole dimensioni la latenza introdotta dai processi di compressione e decompressione vanificherà i nostri sforzi, se non addirittura peggiorerà le prestazioni. Come già mostrato nei precedenti esempi non ha neppure senso applicare la compressione ad alcune tipologie di file che per propria natura non possono essere ulteriormente compressi. In tutti gli altri casi, invece, si ridurrà il numero di pacchetti tcp scambiati, da cui connessioni più brevi con i client, meno processi di Apache attivi per lungo tempo, meno fork, minore utilizzo di memoria. Il tutto a scapito di un contenuto maggiore carico di CPU, che nella maggior parte delle situazioni risulterà meno importante del corrispondente risparmio di RAM.
E che dire dei proxy server? Questi, interponendosi tra web server e browser, potrebbero creare problemi fornendo contenuti compressi a client incapaci di processarli. Fortunatamente mod_deflate invia automaticamente una particolare intestazione HTTP:
Vary: Accept-Encoding
In tal modo i proxy sapranno che la risorsa salvata nella propria cache andrà fornita solo ai client in grado di gestirla, ovvero quelli che invieranno la corrispondente intestazione di Accept-Encoding.
Una piccola digressione finale lato programmatore web: i file CSS, JavaScript ed anche HTML possono essere "minimizzati" eliminando spazi, tabulazioni, ritorni a capo, commenti. Per far ciò sono disponibili vari tool gratuiti, alcuni addirittura in real-time, tra questi va sicuramente nominato YUI Compressor di Yahoo. I vantaggi sono analoghi a quelli analizzati per la compressione lato server, non solo, anche quest'ultima ne beneficerà agendo con maggiore efficienza.
Il traffico in upload
Sino ad ora abbiamo ragionato ed agito sui dati in transito dal server verso il client, ma la comunicazione è biunivoca. Lo scambio di dati che dal client va al server, pur essendo più difficilmente controllabile, ha comunque una notevole importanza. Data la comune assimetricità della banda Adsl a sfavore dell'upload questo fattore può diventare non trascurabile.
Nelle applicazioni web i cookie vengono largamente utilizzati per memorizzare impostazioni e preferenze degli utenti oppure per gestirne l'autenticazione. Le informazioni salvate nei cookie vengono comunicate tramite le intestazioni HTTP, mantenere la loro dimensione limitata ridurrà i dati in transito. In questo senso può risultare estremamente vantaggioso sfruttarli solo per referenziare mediante ID univoco informazioni salvate sul server. Anche impostare in maniera ponderata la loro durata, senza che continuino a sopravvivere inutilmente, alleggerirà le intestazioni HTTP del client.
D'accordo, tutto questo è responsabilità del programmatore, ma anche l'amministratore del server può dare il suo contributo. Possiamo ad esempio domandarci se abbia senso che, nel richiedere un'immagine il client invii con i suoi header tutte le informazioni relative ai cookie. Sicuramente no, creano traffico in eccesso senza alcuna ragione. Per evitarlo possiamo creare un sottodominio cookie-free per ospitare tutte quelle risorse statiche che non hanno bisogno di portare con sé particolari informazioni. Immaginando che le nostre pagine si trovino sul dominio www.miosito.it potremo configurare DNS ed Apache perché vengano fornite immagini, fogli di stile, script da static.miosito.it.
Tale accortezza ci avvantaggerà anche con i proxy server, questi ultimi, infatti, non dovrebbero memorizzare nella propria cache risorse richieste con l'accompagnamento di cookie, per ovvi motivi di riservatezza. Non tener conto di questo significherà rinunciare ai vantaggi, in precedenza già analizzati, che una cache pubblica ci offre.
Alla luce di quanto detto è meglio non configurare il nostro sito perché risponda all'indirizzo miosito.it omettendo il www. In questo caso i cookie impostati a livello di miosito.it interesseranno tutti i sottomini compreso static.miosito.it. Avremo quindi bisogno di registrare un secondo nome a dominio per creare il nostro spazio cookie-free.
Utilizzare una CDN
Una CDN, Content Delivery Network, è costituita da un insieme di web server distribuiti geograficamente in diverse località del globo. Il nostro sito risulterà replicato su tutte le macchine disponibili. Poiché la distanza dei visitatori può avere un impatto sui tempi di risposta, i contenuti richiesti verranno forniti dal server più prossimo al navigatore. I criteri applicati saranno non tanto quelli di distanza fisica, ma di network proximity, ad esempio il server con la risposta più rapida o quello che richiede meno salti, hops.
È evidente che un simile approccio all'ottimizzazione non sia alla portata di tutti. Alcune grandi aziende posseggono una propria CDN, in alternativa è possibile appoggiarsi a dei CDN service provider, ma per la maggior parte delle realtà i costi risultano proibitivi.
Pur avendo le possibilità economiche di implementare una simile strategia, è necessario valutare bene l'impatto di una modifica dell'architettura del proprio sito per adattarlo alla distribuzione. Si pensi a problemi come l'allineamento dei database, la sincronizzazione delle sessioni, tutte operazioni non banali e che comunque possono inserire latenze tali da vanificare i vantaggi della CDN. Certo è che un colosso come Yahoo riferisce di aver migliorato i tempi di risposta del 20% con l'adozione di questa soluzione tecnica.
Nel nostro piccolo possiamo comunque sfruttare, almeno parzialmente, i vantaggi di una CDN approfittando di quelle pubbliche. Molti framework open source, ad esempio jQuery, vengono infatti ospitati dalle CDN di grandi aziende come Google o Microsoft. Meglio referenziare nelle nostre pagine una copia distribuita con tale meccanismo anziché una copia locale. Oltretutto, dato che più siti utilizzano questa tecnica, è maggiore la probabilità che il browser del nostro visitatore abbia già il codice in cache.
In queste pagine abbiamo analizzato alcune soluzioni su cui ragionare nel processo di miglioramento delle prestazioni dei nostri siti web. Spero sia risultato chiaro come non si debba considerare una certa strategia positiva in assoluto, ma vada valutata e testata nel caso specifico. Prossimamente vedremo come sfruttare il modulo di Apache, mod_pagespeed, per alleggerirci un po' del fardello dell'ottimizzazione.