sept. 22 2010

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

Category: ASP.NETNicolas Esprit @ 21:24

Voici le deuxième article de la série sur l'optimisation des performances en ASP.NET. Le précédent billet expliquait comment compresser et mettre en cache des images via un HttpHandler. Aujourd'hui nous allons nous intéresser à la gestion des fichiers Javascript et CSS. Au programme :

  • Minify (Minification)
  • Compression
  • Mise en cache

Minifier un fichier

Un fichier qu'il soit écrit en Javascript ou qu'il soit une feuille de style est avant tout du texte rédigé par un développeur (ou designer). Ainsi lors de nos développements nous utilisons des retours à la ligne, des tabulations, des commentaires et surtout des espaces pour faciliter la lecture et la maintenance du code. Seulement ces ajouts ne sont utiles qu'à l'homme et ne font qu'augmenter la taille des fichiers que le navigateur doit télécharger pour afficher une page lors d'une requête.  

On appelle minifier un fichier le fait de retirer tous les caractères superflus sans altérer les fonctionnalités du code de celui-ci. Prenons un exemple avec cette simple méthode Javascript :

// Afficher "Zéro" si valeur vaut 0, "Autre chose" sinon
function AfficherMessage(valeur) {
if (valeur == 0) {
alert('Zéro');
}
else {
alert('Autre chose');
}
}

 

Pour le navigateur, le code ci-dessous aura le même résultat :

function AfficherMessage(valeur){if(valeur==0){alert('Zéro');}else{alert('Autre chose');}}

Ainsi minifier un fichier permet de réduire sa taille de façon drastique. La réduction dépend avant tout du nombre de commentaires, du style d'écriture du développeur et donc peut varier selon les fichiers. Mais on peut en général espérer plus de 50% de gain. Dans notre exemple, nous avions 186 caractères au départ et 89 à l'arrivée, soit un taux de réduction de 52%.

Bien entendu minifier un fichier n'est pas une opération manuelle. D'une part parce que cela prend trop de temps et d'autre part parce qu'un simple oubli comme celui d'un point-virgule peut causer beaucoup de dégats. Pour minifier vos fichiers vous avez trois possibilités :

  • Minifier manuellement les fichiers avant déploiement sur le serveur Web via un outil.
  • Utiliser un outil automatisé.
  • Utiliser un HttpHandler.

Pour Minifier manuellement vos fichiers, vous pouvez utiliser des outils en ligne gratuits comme : JsCompress, JS Minifier, Javascript Compressor, ou encore YUI Compressor. Ensuite charge à vous de remplacer vos fichiers originaux par les fichiers minifiés lors du déploiement (pensez à bien sauvegarder les originaux). Si cette technique vous intéresse, je vous conseille de lire ce comparatif : Yahoo UI Compressor vs Microsoft AJAX Minifier vs Google Closure Compiler.

Si vous souhaitez automatiser le minifying de vos fichiers, plusieurs solutions s'offrent à vous mais elles correspondent toutes au même schéma : utiliser un algo pour minifier le fichier et automatiser son lancement lors d'un build. Vous pouvez utiliser MsBuild ou encore Nant ou tout autre système. Si cette option vous intéresse je vous conseille de consulter le très connu YUI Compressor sur CodePlex qui correspond au portage du projet Java éponyme de Yahoo!. Il y a également un tutoriel vous permettant d'utiliser YUI lors des évènement post-build dans Visual Studio.

Nous allons maintenant étudier plus en détails la dernière solution, à savoir l'utilisation d'un HttpHandler.

 

Utiliser un HttpHanlder pour minifier les fichiers Javascript

Comme vu dans le précédent billet concernant les images, nous allons mettre en place un HttpHandler afin de gérer les fichiers Javascript. Cet HttpHandler aura pour rôle de minifier nos fichiers, les compresser puis de les mettre en cache. Un peu de refactoring ne faisant pas de mal, il convient d'externaliser des Handlers la compression et le caching. Pour cela nous allons créer des HttpHelpers : HttpCompression comme ci-dessous

internal static class HttpCompression
{
private const string GZIP = "gzip";
private const string DEFLATE = "deflate";

public static void Compress(HttpContext context)
{
if (context != null)
{
if (IsEncodingAccepted(DEFLATE))
{
context.Response.Filter = new DeflateStream(context.Response.Filter, CompressionMode.Compress);
SetEncoding(DEFLATE);
}
else if (IsEncodingAccepted(GZIP))
{
context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
SetEncoding(GZIP);
}
}
}

private static bool IsEncodingAccepted(string encoding)
{
return HttpContext.Current.Request.Headers["Accept-encoding"] != null && HttpContext.Current.Request.Headers["Accept-encoding"].Contains(encoding);
}

private static void SetEncoding(string encoding)
{
HttpContext.Current.Response.AppendHeader("Content-encoding", encoding);
HttpContext.Current.Response.Cache.VaryByHeaders["Accept-encoding"] = true;
}
}

 

et HttpCache :

internal static class HttpCache
{
public static void SetHeaders(HttpContext context, FileInfo file, string contentType, double cachingTimeSpan, HttpCacheability cacheability, string fileName)
{
context.Response.ContentType = contentType;
context.Response.Cache.SetCacheability(cacheability);
context.Response.Cache.SetExpires(DateTime.Now.AddDays(cachingTimeSpan));
context.Response.AddHeader("content-disposition", "inline; filename=" + fileName);
context.Response.AddFileDependency(file.FullName);
context.Response.Cache.SetValidUntilExpires(true);
}
}

 

De même, vu que nous utilisons maintenant plusieurs HttpHandlers aux caractéristiques communes, cette classe de base sera rajoutée au projet :

public abstract class BaseHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}

public abstract void ProcessRequest(HttpContext context);
}

Afin de ne pas réinventer la roue, nous allons reprendre le code de Douglas Crockford pour minifier le code Javascript. Au final, notre HttpHandler sera le suivant :

public class JavascriptMinifyingHandler : BaseHandler
{
public override void ProcessRequest(HttpContext context)
{
string fileName = context.Request.AppRelativeCurrentExecutionFilePath;
string extension = fileName.Substring(fileName.LastIndexOf('.') + 1);
AspNetPerformanceSection config = AspNetPerformanceConfiguration.GetMinifyingConfiguration(context);
FileInfo file = new FileInfo(context.Server.MapPath(fileName));

if (file.Exists)
{
if (config != null && config.FileExtensions[extension] != null)
HttpCache.SetHeaders(context, file, config.FileExtensions[extension].Extension, config.CachingTimeSpan, HttpCacheability.Public, fileName);

HttpCompression.Compress(context);
WriteContent(context, file.FullName, !fileName.EndsWith("min." + extension));
}
else
context.Response.StatusCode = 404;
}

private static void WriteContent(HttpContext context, string file, bool minify)
{
using (StreamReader reader = new StreamReader(file))
{
string body = reader.ReadToEnd();

if (minify)
body = new JavaScriptMinifier().Minify(body);

context.Response.Write(body);
}
}
}

Lors d'une requête, nous vérifions que le fichier demandé existe. Si ce n'est pas le cas une erreur 404 est renvoyée au navigateur. Ensuite, selon les options spécifiées dans la section de configuration de notre outil dans le web.config, nous procédons à l'ajout des headers pour le cache grâce au HttpHelper HttpCache. Après, même combat pour la compression avec la classe HttpCompression. Enfin, dans la méthode WriteContent de notre Handler nous procédons au minifying. Toutefois, il se peut que dans notre projet nous utilisions des fichiers javascript déjà minifiés (comme c'est le cas avec JQuery UI). Dans ce cas nous vérifions que le nom du fichier n'est pas suffixé par "min" avant le lancer le minifier.

 

Utiliser un HttpHanlder pour minifier les fichiers CSS

Pour gérer les feuilles de style la méthode est la même. Le seul changement concerne le minifier car les optimisations pour les css ne sont pas les même que pour le Javascript. Ainsi, nous pouvons retravailler notre JavascriptMinifyHandler et le renommer simplement MinifyHandler. Selon l'extension du fichier demandé par le navigateur nous utiliserons le minifier adéquat. Au terme de ces rajouts, notre fichier web.config ressemble à ceci :

<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>
<Minifying CachingTimeSpan="100">
<FileExtensions>
<add Extension="js" ContentType="text/javascript" />
<add Extension="css" ContentType="text/css" />
</FileExtensions>
</Minifying>
</AspNetPerformance.Util>

<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"/>
<add verb="*" path="*.js" type="AspNetPerformance.Util.Minifying.MinifyingHandler, AspNetPerformance.Util"/>
<add verb="*" path="*.css" type="AspNetPerformance.Util.Minifying.MinifyingHandler, AspNetPerformance.Util"/>
</httpHandlers>
</system.web>

Dans le prochain billet nous reviendrons sur les fichiers Javascript et CSS et plus précisément nous verrons comment et pourquoi il est intéressant de les combiner. Ce sera également l'occasion de revoir un peu nos différentes sections de configuration ainsi que nos Handlers. Je ne sais pas encore juqu'où ira cette série mais le but serait d'obtenir au final un outil d'amélioration des performances facile à mettre en oeuvre et à configurer et réutilisable pour les nouveaux projets.

Test de l'outil

Pour réaliser nos tests nous allons utiliser plusieurs fichiers Javascript et plusieurs fichiers CSS. Afin de faire simple, la page se verra ajouter deux contrôles JQueryUI : l'accordion et une démonstration des différents effets. Voici notre page et notre solution dans Visual Studio :

 

Si nous désactivons les Handlers pour améliorer les performances, voici tout ce que devra charger notre navigateur :

 

On peut constater que onze requêtes seront nécessaires. Avec les fichiers Javascript volumineux ainsi que les CSS ont atteint tout de même 417K. Regardons maintenant le temps qu'il faudra au navigateur pour charger tout ces fichiers :

 

Presque quatres secondes... Regardons maintenant ce qu'ils se passe une fois les outils de performance activés :

La première requête nécessitera bien entendu de charger tous les fichiers vu que ceux-ci ne sont pas encore dans le cache du navigateur (d'où l'utilité de la combinaison de fichiers que nous verrons dans le prochain billet). Toutefois ces requêtes représentes un poids de 110K contre 417K précédement, soit un gain de 75% environ. Enfin, passé la mise en cache il ne faudra plus qu'une seule requête de 4.3K pour charger notre page (et encore, la compression HTML n'est pas en place). Pour comparer le temps de chargement, le tableau ci-dessous résume bien la chose :

 

C'est la fin de ce deuxième billet. N'hésitez pas à laisser un commentaire ou proposer une amélioration que je prendrais en compte lors du prochain billet. Je vois déjà venir les remarques sur les nouvelles versions des fichiers javascript lors d'une release qui ne sont pas pris en compte par les navigateurs car il existe déjà une ancienne version en cache : j'y reviendrais aussi dans le billet suivant :-)

Les sources sont téléchargeables ici.

Tags: , , , , ,

Commentaires

1.
William Anstett William Anstett France says:

Superbe astuce, merci pour ce billet !

2.
trackback Nicolas Esprit says:

[Performance] LinqToSql vs SqlCommand vs SqlBulkCopy

[Performance] LinqToSql vs SqlCommand vs SqlBulkCopy

Les commentaires sont clos