sept. 15 2010

[ASP.NET] Performance Tips 1 : Compresser et mettre en cache les images

Category: ASP.NETNicolas Esprit @ 01:35

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: , , , ,

Commentaires

1.
trackback Nicolas Esprit says:

[ASP.NET] Performance Tips 2 : Minifier, compresser et mettre en cache des fichiers Javascript et CSS

[ASP.NET] Performance Tips 2 : Minifier, compresser et mettre en cache des fichiers Javascript et CSS

2.
Alessandri Olivier Alessandri Olivier France says:

Très bon article, très clair, félicitation. Il est effectivement important de développer en optimisant les peformances et il est vrai qu'avec ASP.Net Webform ce n'est pas forcément évident avec ce qu'il génère.

J'aurais une question liées à vos 2 tips sur la performance :Est ce que l'optimisation tiens compte des ScriptResource.axd ou WebResource.axd généré par le .Net? Car quand on utilise l'ajaxtoolkit qui est assez lourd il serait bon de l'optimiser.




3.
Nicolas Esprit Nicolas Esprit France says:

Merci Smile

Concernant les scripts provenant de ScriptResource.axd et WebResource.axd, ceux-ci ne sont pas pris en compte par les tips des deux billets. Tout simplement parce que ScriptResource et WebResource sont eux aussi des HttpHandler pour les scripts.
Cependant, il est tout à fait possible d'overider cela afin d'optimiser l'utilisation d'ASP.NET Ajax et de l'AjaxControlToolkit. Mais ce sera l'objet d'un autre billet Smile

4.
Emmanuel Humez Emmanuel Humez France says:

Très bon article également !

Dans le response du context, on peut aussi utiliser la méthode SetMaxAge() avec un TimeSpan en argument.
Mais dans ce cas, cela va overrider le SetExpires.
On utilisera plus le SetExpires pour des ressources statiques (images, css, etc.), et le SetMaxAge pour une page entière.

5.
Emac Emac France says:

Petite interrogation tout de même sur le sujet.

J'ai téléchargé les sources pour tester et je me rend compte qu'a chaque réactualisation de la page, la durée d'expiration d'une image se met à jour comme si l'image était recompressé et remis en cache.

Quand je vais voir le code, je m'apperçois que tu ne teste effectivement pas si l'image compréssé est déjà dans le cache. Est ce normal ?

Les commentaires sont clos