DarioSantarelli.Blog("UgiDotNet");

<sharing mode=”On” users=”*” />
posts - 176, comments - 144, trackbacks - 3

My Links

News


This is my personal blog. These postings are provided "AS IS" with no warranties, and confer no rights.




Tag Cloud

Archives

Post Categories

My English Blog

Migliorare le performance di un’applicazione ASP.NET MVC

Esistono molti aspetti da tenere in considerazione quando si parla di performance di applicazioni Web. Infatti, le problematiche legate al protocollo HTTP spesso scavalcano il prode tentativo di scrivere codice server-side e client-side ottimizzato, poiché migliorare le performance di un’ applicazione Web significa anche minimizzare il traffico dati tra client e server. Le best practices per velocizzare il caricamento delle pagine web non sono poche, come si può appurare semplicemente consultando l’utilissimo documento di Yahoo Developer Network dal titolo “Best Practices for Speeding Up Your Web Site”  (parliamo di 35 best practices divise in 7 categorie!)
In questo post ho cercato di aggregare una serie di appunti/approfondimenti relativi a tecniche comuni di miglioramento delle performance di un’applicazione ASP.NET MVC (e non solo) in base alla mia esperienza. I punti su cui cercherò di soffermarmi in particolare sono:

  • Output Cache
  • Compressione HTTP 
  • Minimizzazione e combinazione di risorse esterne (JavaScript e Css) 
  • Minimizzazione del markup HTML 
  • Ottimizzazione della generazione degli URL
  • Note di configurazione


Output cache

Un’ applicazione Web dovrebbe sempre prevedere una politica di caching delle risposte HTTP sia client-side (browser + proxy) che server-side, in modo tale che il contenuto di una risposta HTTP non venga rigenerato inutilmente ogni volta che il client ne effettua la richiesta. Il modo più semplice ed immediato per abilitare il caching in un’applicazione MVC è aggiungere l’attributo [OutputCache] a livello di action. Questa funzionalità è praticamente identica a quella che incontriamo nelle Web Forms, ma cambia nel fatto che non deve essere applicata a livello di View (quindi non dovremmo usare la direttiva <%@ OutputCache %>), anche perché non avrebbe molto senso nel pattern MVC, in cui la View dovrebbe essere agnostica rispetto alle politiche di caching. L’unica proprietà che non è supportata nella versione MVC è VaryByControl.
In genere è sempre consigliabile cercare di estrarre le impostazioni di caching nel web.config definendo dei profili, in modo da non dover ricompilare il sorgente ogni volta che cambiamo le nostre strategie di caching. Quindi, nel controller dovremmo scrivere qualcosa del genere:

public class HomeController : Controller
{
[OutputCache(CacheProfile="MyCacheProfile")]
public ActionResult Index()
{                            
return View();
}

}


e nel web.config

<configuration> 
<
system.web>
   <caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="MyCacheProfile" enabled="true" duration="600" varyByParam="None" location="Any"
/>
          ...

</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
</configuration>


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.

  • Impostare <compilation debug="false" /> nel web.config della nostra applicazione nell’ambiente di produzione, in modo tale che il codice venga eseguito più velocemente.
  • Rimuovere i moduli HTTP che non sono utilizzati. Nel machine.config o nel root-level web.config del framework troviamo una configurazione di default che specifica il caricamento di diversi moduli che invece potremmo non usare nella nostra applicazione. Nel web.config possiamo quindi esplicitare l’esclusione dei moduli di cui non abbiamo bisogno dalla pipeline di ASP.NET (sia classica che integrata), facendo attenzione a non rompere le dipendenze tra i vari moduli.

    (Pipeline classica)

    <httpModules>
          <remove name="Session"/>
          <
    remove name="WindowsAuthentication"
    />
          <remove name="PassportAuthentication"/>
          ...     
               
    </
    httpModules>

    (Per la pipeline integrata il discorso è analogo. Basta posizionarsi nella sezione system.webServer\modules).

HTH

Tag di Technorati: ,,

Print | posted on martedì 11 gennaio 2011 20.31 | Filed Under [ ASP.NET MVC ]

Feedback

Gravatar

# Migliorare le performance di un’applicazione ASP.NET MVC

Esistono molti aspetti da tenere in considerazione quando si parla di performance di applicazioni Web
11/01/2011 21.12 | ExternalBlogs
Gravatar

# re: Migliorare le performance di un’applicazione ASP.NET MVC

Complimenti bellissimo post
13/01/2011 17.00 | Alessage
Gravatar

# re: Migliorare le performance di un’applicazione ASP.NET MVC

Ti ringrazio! :D
13/01/2011 18.41 | Dario Santarelli

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 6 and 3 and type the answer here:

Powered by: