nov. 02 2010

[ASP.NET] Performance Tips 3 : Combiner des fichiers Javascript et CSS

Category: ASP.NET | JavascriptNicolas Esprit @ 18:27

Voici le troisième article de la série sur l'optimisation des performances en ASP.NET. Vous pouvez retrouver les deux précédents billets :

Aujourd'hui nous allons voir comment combiner des fichiers javascripts et CSS. Les bonnes pratiques de développement web nécessitent souvent d'utiliser de nombreux fichiers javascripts et CSS. Afin de faciliter la lisibilité du code et la maintenance, on peut par exemple regrouper toutes les fonctions javascripts qui concernent le contrôle de la saisie dans un fichier, et toutes les fonctions qui concernent l'UI dans un autre fichier. Si on ajoute les bibliothèques javascript annexes (telles que JQuery ou JQuery.UI), ainsi que les différents fichiers CSS, on peut vite se retrouver avec 10 fichiers à télécharger par page.

Bonne pratique de développement oui, bonne pratique en termes de performance non. Chaque fichier javascript ou CSS requis pour l'affichage d'une page nécéssite une requête HTTP. Ainsi pour 10 fichiers, nous aurons 10 aller-retours entre le navigateur et le serveur Web. Comme vous le savez, le temps pour télécharger un élément via HTTP déprendra avant tout de la taille de celui-ci mais aussi de votre vitesse de chargement. Cependant, il ne faut pas oublier le temps de latence. Supposons que celui-ci soit de 50ms, nous aurons donc une demie-seconde imputable au temps de latence pour le téléchargement des 10 fichiers. Et une demie-seconde, c'est trop !

Pour remédier à cela une solution existe : la combinaison de fichiers. Nous allons voir dans ce billet comment combiner plusieurs fichiers javascripts ou CSS en un seul. En ce qui concerne les images, la solution préconisée est d'utiliser des sprites CSS.

Pour faire simple, nous allons reprendre la page Default.aspx du précédent billet de la série. Afin de mieux illustrer la combinaison de fichiers :

<head runat="server">
<title>ASP.NET Performance</title>
<link rel="stylesheet" href="../Css/jquery-ui-1.8.5.custom.css" />
<link rel="stylesheet" href="../Css/DefaultStyle.css" />
<link rel="stylesheet" href="../Css/DefaultStyle2.css" />
<link rel="stylesheet" href="../Css/DefaultStyle3.css" />
</head>

 

Cinq fichiers javascripts seront également référencés dans notre page :

<script src="Scripts/jquery-1.4.2.js" type="text/javascript" />
<script src="Scripts/jquery-ui-1.8.5.custom.min.js" type="text/javascript" />
<script src="Scripts/CustomScript.js" type="text/javascript" />
<script src="Scripts/CustomScript2.js" type="text/javascript" />
<script src="Scripts/CustomScript3.js" type="text/javascript" />

 

Une fois notre page de départ définie, voici comment nous allons procéder pour combiner tous ces fichiers. L'idée est de regrouper toutes les références au même type de fichier (javscript ou CSS) en une seule, et d'utiliser un HttpHandler pour gérer la requête sur les fichiers à combiner. Pour ce faire, nous allons remplacer les références CSS précédentes par ceci :

<%=AspNetPerformance.Util.Combiner.CombinerHandler.GetStyleSheetTag("DefaultCombined.aspx","1")%>

 

Même chose pour les fichiers javascript :

<%=AspNetPerformance.Util.Combiner.CombinerHandler.GetJavascriptTag("DefaultCombined.aspx","1")%>

Le HttpHandler CombinerHandler que nous allons coder contiendra les deux méthodes statiques suivantes. Ces méthodes permettent simplement de créer la nouvelle référence aux fichiers javascript ou CSS qui remplacera les précédentes :

public static string GetJavascriptTag(string pageCode, string version)
{
return String.Format("<script type=\"text/javascript\" src=\"CombinerHandler.axd?v={0}&type=js&pcode={1}\"></script>", version, pageCode);
}
public static string GetStyleSheetTag(string pageCode, string version)
{
return String.Format("<link link href=\"CombinerHandler.axd?v={0}&type=css&pcode={1}\" rel=\"stylesheet\" type=\"text/css\"/>", version, pageCode);
}

 

Deux paramètres sont attendus : pageCode et version. Le premier nous permettra de savoir quels sont les scripts à combiner, le plus simple étant d'utiliser le nom de la page en cours de traitement pour cela. (il est aussi possible d'avoir des références javascripts en début de page et en fin de page, à nous de les différencier via un pageCode unique). Le second permettra de différencier les versions de notre combinaison (si par exemple on ajoute ou supprime une référence à un fichier, il faut que le nouveau fichier résultant de la combinaison sont bien pris en compte et non pas tiré du cache).

Afin que notre référence sur CombinerHandler.axd fonctionne, nous ajoutons le HttpHandler dans le fichier Web.Config de notre application Web :

<httpHandlers>    
...
<add verb="POST,GET" path="CombinerHandler.axd" type="AspNetPerformance.Util.Combiner.CombinerHandler, AspNetPerformance.Util"/>
</httpHandlers>

 

Vous devez vous demander : “c’est bien beau de remplacer quatre références CSS par une référence à un HttpHandler, mais comment cet Handler saura quels fichiers sont à combiner ?”. La réponse est simple : nous utiliserons pour cela un fichier XML qui permettra de paramétrer les références pour nos pages. Afin de favoriser la lisibilité et la maintenance, nous utiliserons un fichier XML pour les références javascript et un autre pour les références CSS. Ci-dessous un aperçu pour les CSS :

<?xml version="1.0" encoding="utf-8" ?>
<scripts>
<page pagecode="DefaultCombined.aspx" name="ASP.NET Performance">
<script>~/Scripts/jquery-1.4.2.js</script>
<script>~/Scripts/jquery-ui-1.8.5.custom.min.js</script>
<script>~/Scripts/CustomScript.js</script>
<script>~/Scripts/CustomScript2.js</script>
<script>~/Scripts/CustomScript3.js</script>
</page>
</scripts>

 

Voilà, tout est en place, il ne nous reste plus qu’à coder le HttpHandler afin qu’il puisse combiner, minifier, compresser et mettre en cache nos fichiers. Comme nous l’avons vu dans les précédents billets de cette série, nos efforts se concentrent sur la méthode ProcessRequest. Nous réutiliserons autant que faire se peut les méthodes précédemment codées (notamment celles des classes HttpCache, HttpCompression et les Minifier javascript et CSS). Ci-dessous, le code de notre principale méthode :

public override void ProcessRequest(HttpContext context)
{
string type = (!String.IsNullOrEmpty(context.Request["type"])) ? context.Request["type"] : null;
string version = (!String.IsNullOrEmpty(context.Request["v"])) ? context.Request["v"] : null;
string pageCode = (!String.IsNullOrEmpty(context.Request["pcode"])) ? context.Request["pcode"] : null;

string combinedContent = GetCombinedContentFromFiles(context, pageCode, type, version);

HttpCache.SetHeaders(context, AspNetPerformanceConfiguration.GetMinifyingConfiguration(context), type, HttpCacheability.Public);
HttpCompression.Compress(context);
WriteContent(context, combinedContent, type.Equals("js"));
}

 

Les trois premières lignes permettent de récupérer les QueryString utilisées dans nos références, à savoir : le type de fichier demandé (javascript ou CSS), la version de la combinaison, et le pageCode (le nom de la page dans notre exemple) de la combinaison de fichiers à réaliser. Une fois ces informations récupérées, nous appelons la méthode GetCombinedContentFromFiles qui va nous renvoyer le contenu combinés de tous les fichiers définis pour le pageCode. Enfin, comme vu dans le précédent billet, ajout des headers pour le cache, compression et minification du résultat (via la méthode WriteContent).

Au passage, nous devrions utiliser le cache ASP.NET (HttpContext.Current.Cache) pour stocker le résultat combiné des fichiers référencés, afin de ne plus avoir à le refaire. Vu les possibilités offertes par le cache serveur, celui-ci fera l’objet d’un prochain billet.

private static string GetCombinedContentFromFiles(HttpContext context, string pageCode, string type, string version)
{
string setName = type.Equals("js") ? "script" : "style";
string filename = HttpContext.Current.Server.MapPath(String.Format("~/App_Data/{0}s.xml", setName));

XPathDocument xmlDoc = new XPathDocument(filename);
XPathNavigator navigator = xmlDoc.CreateNavigator();
XPathNodeIterator iterator = navigator.Select(string.Format("/{0}s/page[@pagecode='{1}']/{2}", setName, pageCode, setName));

StringBuilder combinedSource = new StringBuilder();
while (iterator.MoveNext())
combinedSource.Append(System.IO.File.ReadAllText(context.Server.MapPath(iterator.Current.InnerXml), Encoding.UTF8));

return combinedSource.ToString();
}

 

La méthode GetCombinedContentFromFile n'a rien de compliqué. Une requête XPath va permettre de récupérer la liste des fichiers à combiner dans le fichier XML, correspondant aux paramètres pageCode et type. Une fois la liste obtenue, on boucle sur tous les fichiers afin de les lire et de les ajouter à un StringBuilder (qui comme vous le savez surement déjà, mais un rappel ne faisant pas de mal, est beaucoup plus performant que la concaténation de string puisqu'il évite les allocations mémoires à tout va).

Enfin, le code ci-dessous montre le fonctionnement de la méthode WriteContent. Ici, même principe que dans le billet précédent : selon le type de contenu on appelle le bon minifier.

private static void WriteContent(HttpContext context, string body, bool isJavaScript)
{
if (isJavaScript)
body = new JavaScriptMinifier().Minify(body);
else
body = new CssMinifier().Minify(body);

context.Response.Write(body);
}

 

Passons maintenant aux tests afin de mesurer l'apport de la combinaison de fichiers. Pour cela nous allons comparer le chargement de la page Default.aspx, qui comporte cinq références javascripts et quatres références CSS, avec et sans combinaison. Sans celle-ci, le résultat est le suivant :

Réseau sans combinaison de fichiers

 

Le temps de chargement de la page n'est pas vraiment représentatif puiqu'elle est executée en local. Mais notons tout de même les 6,3 secondes nécessaires. Ci-dessous, nous notons que, sans utiliser le cache du navigateur, 15 requêtes HTTP sont nécessaires au chargement de notre page :

Statistiques sans combinaison de fichiers

 

Activons maintenant la combinaison. Le temps de chargement de la page passe alors de 6,3 à 4,5 secondes. Même si ce temps de chargement n'est pas représentatif, on note une nette amélioration.

Statistiques avec combinaison de fichiers

 

Enfin, on peut constater que l'on passe de 15 à 8 requêtes HTTP lorsque la combinaison de fichiers est activée. Vous remarquerez au passage que la taille totale des scripts et des CSS a diminuée. Ceci s'explique par la compression qui est plus performante pour un seul fichier que pour plusieurs. Egalement, la taille du code HTML a quelque peu diminué : on passe de 4,5 à 4,2K. Ceci est simplement du au passage de 8 références (balises <link> et <script>) à 2.

Réseau aveccombinaison de fichiers

Ainsi s'achève ce troisième billet de la série sur l'amélioration des performances en ASP.NET. Les sources sont téléchargeables ici.

Tags: , , , ,

Commentaires

1.
pingback topsy.com says:

Pingback from topsy.com

Twitter Trackbacks for
        
        [ASP.NET] Performance Tips 3 : Combiner des fichiers Javascript et CSS
        [nicolasesprit.com]
        on Topsy.com

2.
florian florian France says:

Bonjour,

Petite question, quelles sont les modifs  a apporter au code ci dessous pour le rendre fonctionnel ? car dans mon projet, la methode CombinerHandler.ProcessRequest n est jamais appelee. le fichier web.config a pourtant ete modifie. une idee ?

Un grand merci

Les commentaires sont clos