Ovviamente il controllo programmatico della cache di ASP.NET è sempre a disposizione tramite la solita classe System.Web.Caching.Cache. L’unica e non indifferente limitazione di questo approccio risiede nel fatto che la cache è in-process rispetto all’applicazione web e dunque non è distribuita. In applicazioni che richiedono un alto grado di scalabilità, invece, sarebbe preferibile prendere in considerazione architetture di caching distribuito (es. Velocity). A riguardo, consiglio la lettura di questo articolo di Stephen Walther: ASP.NET MVC Tip #39 – Use the Velocity Distributed Cache.
Compressione HTTP
In conformità alla specifica HTTP/1.1, i web server ed i browser moderni supportano l’elaborazione di contenuti compressi secondo standard di compressione come il GZIP ed il (migliore) DEFLATE al fine di ridurre sensibilmente la banda occupata dal traffico HTTP. Lato server, la soluzione più scalabile per comprimere contenuti statici e dinamici prevede lo sfruttamento delle feature del Web Server piuttosto che della web application, ovviamente a fronte di ragionevoli valutazioni sul carico computazionale che il server deve sostenere soprattutto per gestire la compressione di contenuti dinamici. Nelle versioni di IIS precedenti alla 6.0, la funzionalità di compressione HTTP non è built-in e richiede comunque l’utilizzo di strumenti di terze parti (come XCompress).
In IIS6.0, invece, abbiamo l’introduzione di features di compressione statica (con cache su disco) e dinamica (senza cache su disco) che richiedono una modifica al metabase (%WINDIR%\system32\inetsrv\metabase.xml), al fine di abilitare gli schemi di compressione gzip e deflate su contenuti identificabili solamente in base al tipo di estensione (es. .aspx, .html etc). Per questo motivo, le applicazioni ASP.NET MVC deployate su IIS6 vanno incontro ad una serie di problematiche legate al fatto che le URL senza estensione forniscono un bel 404, ed anche se si implementa un URL rewrite o un “wildcard” mapping la compressione built-in di contenuti dinamici di IIS6.0 non ha più effetto poiché non sussiste alcun match con le estensioni specificate nel metabase. Quindi, se vogliamo avere la compressione sotto controllo, dobbiamo ricorrere ad un HttpModule, come viene spiegato molto bene in questo post.
Il problema viene risolto alla radice a partire da IIS7.0 grazie alla nuova sezione di configurazione <httpCompression>, che va a sostituire le precedenti configurazioni del metabase di IIS6.0 e ci permette di abilitare la compressione in base ai tipi MIME a livello di server, web site o web application attraverso diverse strade: IIS Manager, la command line di Appcmd.exe, il web.config e le API managed. Un grande vantaggio di IIS7+ inoltre è la capacità automatica di interrompere/riprendere la compressione a seconda che il carico di CPU superi/scenda sotto una soglia configurabile (vedi attributi dynamicCompressionDisableCpuUsage e dynamicCompressionEnableCpuUsage).
Nel caso in cui non fosse disponibile la possibilità di accedere alla configurazione del web server, siamo costretti a prendere la strada applicativa. Analogamente al classico mondo ASP.NET, anche in ASP.NET MVC la gestione della compressione dei contenuti può essere ottenuta tramite un HttpModule (ad esempio HttpCompress) in grado di applicare la compressione all’output stream della risposta HTTP. In genere, questa è una soluzione semplice e riusabile (forse meno performante) che permette di non intaccare almeno nella teoria una web application preesitente.
Un’ulteriore possibilità totalmente programmatica per ASP.NET MVC è la realizzazione di un filtro custom che a livello di controller o di singola action vada ad intercettare la risposta HTTP ed applicare al volo la compressione (come farebbe un HttpModule) sfruttando le classi del framework GZipStream e DeflateStream. Ecco un esempio di action filter:
public class CompressFilterAttribute : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
HttpRequestBase request = filterContext.HttpContext.Request;
string acceptEncoding = request.Headers["Accept-Encoding"];
if (string.IsNullOrEmpty(acceptEncoding)) return;
acceptEncoding = acceptEncoding.ToLowerInvariant();
HttpResponseBase response = filterContext.HttpContext.Response;
if (acceptEncoding.Contains("deflate")) // Priority to DEFLATE compression schema
{
response.AppendHeader("Content-Encoding", "deflate");
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
}
else if (acceptEncoding.Contains("gzip"))
{
response.AppendHeader("Content-Encoding", "gzip");
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
}
}
}
Minimizzazione e combinazione di risorse JavaScript e CSS
Un’altra importante tip per ottimizzare il traffico HTTP tra client e server è data dalla minimizzazione e la combinazione delle risorse esterne ad una pagina Web, in modo da ridurre massicciamente il numero complessivo di richieste HTTP generate dal browser. Esiste un progetto molto interessante su CodePlex che affronta questa problematica sia per applicazioni ASP.NET Web Forms che per applicazioni ASP.NET MVC. Si tratta di Combres, una libreria .NET che permette di organizzare JavaScript e CSS in diversi insiemi, ad ognuno dei quali viene associata una sezione di configurazione nel web.config. Le risorse specificate in ciascun insieme vengono minimizzate, combinate, compresse e messe in cache in modo da poter essere trasmesse in un singolo round-trip HTTP. Per informazioni dettagliate sull’utilizzo di questa libreria rimando al completo articolo su CodeProject.
Minimizzazione del markup HTML
Anche la rimozione degli spazi bianchi che si frappongono tra i tag all’interno di un documento HTML può ridurre il tempo di caricamento di una pagina Web, poiché ne riduce le dimensioni nonché ne favorisce il parsing da parte del browser. Anche questa funzionalità può essere ottenuta tramite un modulo HTTP, come questo di Mads Kristensen. In ASP.NET MVC inoltre si può pensare di realizzare un action filter. Molto semplicemente:
public class HtmlWhitespaceFilter : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
HttpResponseBase response = filterContext.HttpContext.Response;
string contentType = response.ContentType.Trim().ToLowerInvariant();
if (contentType == "text/html" || contentType == "application/xhtml+xml" ||
contentType == "text/xml" || contentType == "application/xml")
{
response.Filter = new HtmlWhitespaceFilterStream(response.Filter);
}
}
}
Riporto per completezza l’implementazione della classe HtmlWhitespaceFilterStream, che internamente utilizza delle banali regular expression per eliminare gli spazi vuoti tra i vari tag del documento (X)HTML.
public class HtmlWhitespaceFilterStream : Stream
{
private Stream _stream;
public override bool CanRead { get { return true; } }
public override bool CanSeek { get { return true; } }
public override bool CanWrite { get { return true; } }
public override void Flush() { _stream.Flush(); }
public override long Length { get { return _stream.Length; } }
public override long Position
{
get { return _stream.Position; }
set { _stream.Position = value; }
}
public HtmlWhitespaceFilterStream(Stream stream) { _stream = stream; }
public override int Read(byte[] buffer, int offset, int count) { return _stream.Read(buffer, offset, count); }
public override long Seek(long offset, SeekOrigin origin) { return _stream.Seek(offset, origin); }
public override void SetLength(long value) { _stream.SetLength(value); }
public override void Close() { _stream.Close(); }
public override void Write(byte[] buffer, int offset, int count)
{
string html = System.Text.Encoding.UTF8.GetString(buffer);
html = Regex.Replace(html, @"\s+<", "<", RegexOptions.Singleline);
html = Regex.Replace(html, @">\s+", ">", RegexOptions.Singleline);
byte[] outdata = System.Text.Encoding.UTF8.GetBytes(html);
_stream.Write(outdata, 0, outdata.Length);
}
}
Ottimizzazione della generazione degli URL
Un aspetto che può intaccare le performance di applicazioni ASP.NET MVC stressate da migliaia richieste al minuto è il modo in cui vengono generati gli URL virtuali nelle View tramite gli helper Html.ActionLink(), Html.RouteLink(), Url.Action() e Url.RouteUrl(). In questo post di Chad Moran viene mostrato in dettaglio come un performance test sulla generazione massiva di URL ci dovrebbe spingere verso la rinuncia delle comodità che ci vengono offerte dalle lambda expression e dagli anonymous object.
Sostanzialmente, per beneficiare di performance migliori, dovremmo preferire una sintassi di questo tipo…
Html.ActionLink("Link", "Index", "Home", new RouteValueDictionary { { "name", "Mario" }, { "age", 56 } })
…ad una di questo tipo…
Html.ActionLink("Link", "Index", "Home", new { name = "Mario", age = 56 })
…o ancora peggio (per le performance) di questo tipo …
Html.ActionLink<HomeController>(c => c.Index("Mario",56), "Link")
Come è facile intuire, i tempi di compilazione delle lambda expression e di reflection sugli anonymous objects introducono a run-time delle latenze che rallentano sensibilmente la generazione delle URL (fino ad un ordine di grandezza!) rispetto all’esecuzione su creazioni dirette di RouteValueDictionary. Contrariamente, dal punto di vista dello sviluppo, proprio l’ultimo metodo è preferibile per il type checking a compile-time.
Note di configurazione
In quest’ultima parte del post vorrei semplicemente ricordare delle semplici accortezze che però sono fondamentali per rendere più performante un’ applicazione ASP.NET.
HTH
Tag di Technorati:
MVC,
ASP.NET,
Performance