Ce billet lance une série traitant de l'optimisation des performances des applications ASP.NET. Je n'ai pas encore choisi le contenu des prochains billets, mais ce ne sont pas les idées qui manquent :
- Gestion du Viewstate
- Cache IIS
- Compression IIS
- Content Delivery Network
- Load Balancing
- Pagination
- Minifying
- Pages Asynchrones
- etc.
Aujourd'hui, pour cette première astuce, je vais aborder un sujet simple mais souvent négligé : la compression et la mise en cache des images via ASP.NET. Bien entendu, nous ne sommes pas obligés de compresser les images lors de l'éxécution, nous pouvons le faire à la main avant de déployer un site sur le serveur Web. Ou bien, au lieu d'utiliser des dizaines d'images sur une seule page, nous pouvons utiliser des sprites css afin d'éviter les aller-retours entre le client et le serveur. Mais ce n'est pas le sujet du jour. Je conçois cette série comme une succession d'astuces dans un contexte propre afin de rédiger au final un article complet comparant les différentes méthodes évoquées et détaillant les pour et les contre en fonction d'un contexte donné.
Revenons à nos moutons et prenons un exemple. Notre application se compose d'une seule page sur laquelle sont placées deux images (on ne peut pas faire plus simple) :

Regardons plus en détails le contenu de la réponse à notre requête. Le browser a téléchargé trois éléments : la page HTML, et nos deux images. Soit mine de rien 214Ko. Vous remarquerez qu'aucun de ces trois éléments n'est compressé ni ne possède de date d'expiration.

On peut constater également sur la figure ci-dessous que la première requête (graphe de gauche) est identique aux suivantes (graphe de droite).

Afin de réduire le poids de la page, mais aussi les requêtes au serveur Web via l'utilisation du cache, nous allons mettre en place un mécanisme nous permettant à la fois de compresser et mettre en cache les images .jpg, .png et .gif de notre application Web. Pour cela nous utiliserons un HttpHandler.
Lors d'une requête concernant une image, nous devons d'abord extraire le chemin physique de celle-ci (via Server.MapPath), ainsi que son nom :
public void ProcessRequest(HttpContext context)
{
string file = context.Server.MapPath(context.Request.FilePath);
string filename = file.Substring(file.LastIndexOf('\\') + 1);
[...]
}
Une fois fait, nous pouvons ajouter dans la réponse Http une date d'expiration pour le cache et paramétrer celui-ci
string extension = file.Substring(file.LastIndexOf('.') + 1);
context.Response.Cache.SetExpires(DateTime.Now.AddDays(config.CachingTimeSpan));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetValidUntilExpires(false);
context.Response.ContentType = config.FileExtensions[extension].ContentType;
Nous utilisons trois méthodes :
- SetExpires : pour indiquer au client le temps durant lequel le contenu est valide
- SetCacheability : pour indiquer au client s'il est autorisé à mettre en cache le contenu
- SetValidUntilExpires : pour indiquer si le client doit oui ou non ignorer les entêtes Http envoyées par le serveur qui invalident le cache
Egalement nous devons préciser le ContentType du fichier. La dernière étape consiste à compresser notre image. Il faut tout d'abord vérifier que le client supporte bien la compression Gzip ou Deflate et si oui appliquer un Filter tout en précisant le mode de compression utilisé via l'entête Http "Content-Encoding". Voici le code final de notre HttpHanlder :
namespace AspNetPerformance.Util.Caching
{
public class CachingHandler : IHttpHandler
{
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
string file = context.Server.MapPath(context.Request.FilePath.Replace(".ashx", ""));
string filename = file.Substring(file.LastIndexOf('\\') + 1);
CachingSection config = (CachingSection)context.GetSection("AspNetPerformance.Util/Caching");
if (config != null)
{
string extension = file.Substring(file.LastIndexOf('.') + 1);
context.Response.Cache.SetExpires(DateTime.Now.AddDays(config.CachingTimeSpan));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetValidUntilExpires(false);
if (config.FileExtensions[extension] != null)
{
context.Response.ContentType = config.FileExtensions[extension].ContentType;
string acceptEncoding = context.Request.Headers["Accept-Encoding"];
Stream prevUncompressedStream = context.Response.Filter;
if (acceptEncoding == null || acceptEncoding.Length == 0)
return;
acceptEncoding = acceptEncoding.ToLower();
// defalte
if (acceptEncoding.Contains("deflate") || acceptEncoding == "*")
{
context.Response.Filter = new DeflateStream(prevUncompressedStream, CompressionMode.Compress);
context.Response.AppendHeader("Content-Encoding", "deflate");
}
// gzip
else if (acceptEncoding.Contains("gzip"))
{
context.Response.Filter = new GZipStream(prevUncompressedStream, CompressionMode.Compress);
context.Response.AppendHeader("Content-Encoding", "gzip");
}
}
}
context.Response.AddHeader("content-disposition", "inline; filename=" + filename);
context.Response.WriteFile(file);
}
}
}
Pour revenir sur la classe CachingSection évoquée tout à l'heure, il s'agit simplement d'une classe qui hérite de ConfigurationSection et qui permet de paramétrer notre HttpHandler dans le Web.config comme ci-dessous. Vous noterez que l'on pourrait à la volée (et sans devoir changer le code) ajouter le type de fichier .css à notre extension. Les fichiers .css sont aussi à mettre en cache et compresser pour obtenir de meilleurs performances. Pourquoi je ne l'ai pas mis dans l'exemple ? Simplement parce que dans un prochain billet je parlerais du Minifying et donc d'un HttpHanlder propre aux css.
<configSections>
<sectionGroup name="AspNetPerformance.Util">
<section name="Caching" requirePermission="false" type="AspNetPerformance.Util.Caching.CachingSection, AspNetPerformance.Util" />
</sectionGroup>
</configSections>
<AspNetPerformance.Util>
<Caching CachingTimeSpan="100">
<FileExtensions>
<add Extension="gif" ContentType="image\gif" />
<add Extension="jpg" ContentType="image\jpeg" />
<add Extension="png" ContentType="image\png" />
</FileExtensions>
</Caching>
</AspNetPerformance.Util>
Le code de la section de configuration du HttpHanlder :
namespace AspNetPerformance.Util.Caching
{
public class CachingSection : ConfigurationSection
{
[ConfigurationProperty("CachingTimeSpan", IsRequired = true)]
public double CachingTimeSpan
{
get { return (double)base["CachingTimeSpan"]; }
set { base["CachingTimeSpan"] = value; }
}
[ConfigurationProperty("FileExtensions", IsDefaultCollection = true, IsRequired = true)]
public FileExtensionCollection FileExtensions
{
get { return ((FileExtensionCollection)base["FileExtensions"]); }
}
}
public class FileExtensionCollection : ConfigurationElementCollection
{ ... }
public class FileExtension : ConfigurationElement
{
[ConfigurationProperty("Extension", IsRequired = true)]
public string Extension
{
get { return (string)base["Extension"]; }
set { base["Extension"] = value.Replace(".", ""); }
}
[ConfigurationProperty("ContentType", IsRequired = true)]
public string ContentType
{
get { return (string)base["ContentType"]; }
set { base["ContentType"] = value; }
}
}
}
Pour configurer notre HttpHandler de façon à ce qu'il s'occupe des requêtes concernant les trois types d'images désirés, il faut déclarer celui-ci dans la section system.web et system.webServer du fichier web.config :
<system.webServer>
<handlers>
<add name="GIF" path="*.gif" verb="*" type="AspNetPerformance.Util.Caching.CachingHandler, AspNetPerformance.Util" />
<add name="JPG" path="*.jpg" verb="*" type="AspNetPerformance.Util.Caching.CachingHandler, AspNetPerformance.Util" />
<add name="PNG" path="*.png" verb="*" type="AspNetPerformance.Util.Caching.CachingHandler, AspNetPerformance.Util" />
</handlers>
</system.webServer>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<httpHandlers>
<add verb="*" path="*.gif" type="AspNetPerformance.Util.Caching.CachingHandler, AspNetPerformance.Util"/>
<add verb="*" path="*.jpg" type="AspNetPerformance.Util.Caching.CachingHandler, AspNetPerformance.Util"/>
<add verb="*" path="*.png" type="AspNetPerformance.Util.Caching.CachingHandler, AspNetPerformance.Util"/>
</httpHandlers>
</system.web>
Regardons maintenant le résultat sur notre page de départ :

La page qui pesait 214Ko n'en fait plus que 137, les deux images ont bien été compressées et mises en cache. Vous noterez toutefois que la plus petite des deux a vu sa taille augmenter. Ceci est normal vu que la compression Zip ou Deflate ne sera jamais aussi performante que le jpeg (qui lui est un mode de compression avec perte de données)

Enfin nous pouvons voir que lors d'une première requête un client devra télécharger l'intégralité de la page, puis lors de suivantes seul l'HTML sera téléchargé.
En espérant que ce billet vous aura été utile. N'hésitez pas à le commenter ou me faire part de vos idées ou demandes concernant la suite de cette série sur l'optimisation des performances en ASP.NET
Les sources sont téléchargeables ici.
Tags: ASP.NET, Caching, HTML, Compression, Performance