Partager via


À la pointe

Programmation CSS : regroupement et minimisation

Dino Esposito

Dino EspositoSelon un ancien principe de développement Web, si les requêtes sont trop nombreuses, les performances de la page en pâtissent. Si vous connaissez une astuce pour limiter le nombre de requêtes HTTP déclenchées par vos pages Web, n'hésitez pas à la mettre en œuvre. Au fur et à mesure que les pages Web s'emplissent d'un contenu visuel plus riche, le coût du téléchargement des ressources associées, telles que des fichiers CSS, des scripts et des images, s'accroît de manière considérable. Naturellement, la majeure partie de ces ressources peut être mise en cache localement par votre navigateur, mais l'empreinte initiale est parfois problématique à long terme. De plus, des requêtes moins nombreuses et moins volumineuses permettent de limiter la bande passante utilisée, de réduire le temps de latence et d'améliorer la longévité de la batterie. Ces facteurs sont cruciaux dans le cadre de la navigation mobile. Une approche courante pour résoudre ces problèmes consiste à combiner deux actions : le regroupement et la minimisation.

Dans cet article, j'aborderai le regroupement et la minimisation des fichiers CSS dans la perspective unique qu'offrent les outils logiciels disponibles dans ASP.NET MVC 4. Cet article s'inscrit dans la continuité de « Création d'affichages optimisés pour mobile dans ASP.NET MVC 4, 2e partie : utilisation de WURFL » (msdn.microsoft.com/magazine/dn342866) que j'ai publié précédemment.

Notions de base du regroupement et de la minimisation

Le regroupement est le processus qui consiste à rassembler plusieurs ressources distinctes dans une ressource téléchargeable unique. Par exemple, un regroupement peut être constitué de plusieurs fichiers JavaScript ou CSS téléchargés localement sur l'ordinateur en ne faisant qu'une seule requête HTTP vers un point de terminaison ad hoc. La minimisation, quant à elle, consiste en une transformation d'une ressource. Plus précisément, la minimisation est la suppression de tous les caractères superflus d'une ressource textuelle, cette action n'ayant aucun impact sur la fonctionnalité attendue. Il peut s'agir de raccourcir des identifiants ou des fonctions de création d'alias, de supprimer des commentaires, des caractères d'espacement, des nouvelles lignes, et de façon plus générale, tous les caractères qui sont ajoutés pour faciliter la lecture mais prennent de la place et n'ont pas de réelle utilité d'un point de vue fonctionnel.

Le regroupement et la minimisation peuvent être appliqués ensemble, mais restent deux processus indépendants. En fonction du besoin, vous pouvez vous contenter de créer des regroupements ou décider de minimiser des fichiers individuellement. Habituellement, cependant, sur des sites en production, il n'y a pas de raison de ne pas regrouper et minimiser tous les fichiers CSS et JavaScript (à l'exception des ressources communes, telles que jQuery qui sont susceptibles de se trouver sur des réseaux de distribution de contenu (CDN) renommés). Au moment du débogage, en revanche, la situation est bien différente : il est difficile de déchiffrer et de s'y retrouver dans une ressource minimisée ou regroupée. Il peut donc être préférable de ne pas activer le regroupement et la minimisation.

De nombreuses structures fournissent des services de regroupement et de minimisation avec des niveaux d'extensibilité et des ensembles de fonctionnalités légèrement différents. Globalement, elles offrent les mêmes capacités. Le choix de l'une ou de l'autre est donc une simple question de préférence. Si vous écrivez une application ASP.NET MVC 4, le choix logique pour le regroupement et la minimisation est l'infrastructure d'optimisation Web ASP.NET, disponible via un package NuGet (bit.ly/1bS8u4B) et présentée dans la figure 1.

Installing the Microsoft ASP.NET Web Optimization Framework
Figure 1 Installation de l'infrastructure d'optimisation Web ASP.NET

Utilisation du regroupement CSS

De mon point de vue, le meilleur moyen de comprendre la mécanique du regroupement CSS est de commencer avec un projet MVC ASP.NET complètement vide. Cela signifie qu'il faut créer un nouveau projet à partir du modèle Projet vide et supprimer les références et les fichiers inutilisés. Ensuite, admettons que vous ajoutiez un fichier de mise en page qui associe deux fichiers CSS :

<link rel="stylesheet"
  href="@Url.Content("~/content/styles/site.css")"/>

Si vous affichez la page et surveillez son activité sur le réseau à l'aide de Fiddler ou des outils de développement Internet Explorer, vous voyez deux téléchargements en parallèle. Il s'agit du comportement par défaut.

Notez que dans ASP.NET MVC 4, vous pouvez récrire le balisage existant de façon beaucoup plus compacte grâce à la nouvelle fonctionnalité Styles.Render :

@Styles.Render(
  "~/content/styles/site.css",
  "~/content/styles/site.more.css")

Située sous System.Web.Optimization, la classe Styles est beaucoup plus puissante qu'elle n'en a l'air. La méthode Styles.Render prend également en charge les regroupements. Cela signifie que la méthode dispose de plusieurs surcharges, dont une accepte un ensemble d'URL CSS. Une autre surcharge, en revanche, utilise le nom d'un regroupement créé précédemment (j'y reviendrai prochainement). Dans ce cas, elle émet un seul élément <link> et le fait pointer sur une URL autogénérée qui retourne toutes les feuilles de styles regroupées ou minimisées.

Création de regroupements CSS

Habituellement, les regroupements sont créés par programme dans global.asax. Conformément au modèle ASP.NET MVC 4 de code de configuration, vous pouvez créer une classe BundleConfig dans le dossier App_Start et y exposer une méthode d'initialisation statique :

BundleConfig.RegisterBundles(BundleTable.Bundles);

Un regroupement CSS n'est ni plus ni moins qu'un ensemble de feuilles de style. Voici le code permettant de regrouper les deux fichiers CSS mentionnés ci-dessus dans un seul téléchargement :

public class BundleConfig
{
  public static void RegisterBundles(BundleCollection bundles)
  {
    // Register bundles first
    bundles.Add(new Bundle("~/mycss").Include(
      "~/content/styles/site.css",
      "~/content/styles/site.more.css"));
    BundleTable.EnableOptimizations = true;
  }
}

Vous créez une nouvelle classe Bundle et indiquez au constructeur le chemin virtuel servant à référencer le regroupement à partir de la vue ou de la page Web. Vous pouvez également définir le chemin virtuel ultérieurement en utilisant la propriété Path. Pour associer des fichiers CSS au regroupement, vous devez utiliser la méthode Include. Cette méthode reprend un ensemble de chaînes représentant des chemins virtuels. Vous pouvez y référencer les fichiers CSS de manière explicite, comme dans l'exemple précédent, ou indiquer une chaîne modèle comme illustré ici :

bundles.Add(new Bundle("~/mycss")
  .Include("~/content/styles/*.css");

La classe Bundle comporte également une méthode IncludeDirectory qui vous permet d'indiquer le chemin vers un répertoire virtuel donné, et éventuellement vers une chaîne correspondant au modèle et un indicateur booléen afin d'activer la recherche dans les sous-répertoires.

La propriété booléenne EnableOptimization définie dans la classe BundleTable dans l'extrait de code qui précède, répond à la nécessité d'activer le regroupement de façon explicite. Si elle n'est pas activée par programme, le regroupement ne fonctionne pas. Comme nous l'avons dit, le regroupement est une forme d'optimisation, et c'est la raison pour laquelle il est plus logique d'y avoir recours lorsque le site est en production. La propriété EnableOptimization est une bonne façon de configurer le regroupement tel qu'il devrait se présenter en production, mais vous avez intérêt à la désactiver jusqu'à ce que le site soit compilé en mode débogage. Vous pourriez même utiliser le code suivant :

if (!DEBUG)
{
  BundleTable.EnableOptimizations = true;
}

Autres fonctionnalités de regroupement avancées.

En ce qui concerne les regroupements CSS, la minimisation est sans doute le seul élément pertinent. Cependant, la classe BundleCollection est une classe à usage général qui peut également être utilisée pour regrouper les fichiers de script. Plus précisément, la classe BundleCollection dispose de deux fonctionnalités qui méritent d'être mentionnées, même si elles ne sont véritablement utiles que lorsque vous regroupez des fichiers de script, plutôt que des fichiers CSS.

La première fonctionnalité est la définition d'un ordre. La classe BundleCollection dispose d'une propriété appelée Orderer de type IBundleOrderer. Aussi évident que cela puisse paraître, « orderer » est un composant dont la responsabilité est de définir l'ordre dans lequel vous souhaitez que les fichiers soient regroupés pour le téléchargement. Par défaut, il s'agit de la classe DefaultBundleOrderer. Cette classe regroupe les fichiers dans l'ordre obtenu à partir des paramètres définis via FileSetOrderList, qui est une propriété de BundleCollection. FileSetOrderList est conçue pour recueillir plusieurs classes BundleFileSetOrdering. Chacune de ces classes définit un modèle pour les fichiers (par exemple, jquery-*), et l'ordre dans lequel les classes BundleFileSetOrdering sont ajoutées à FileSetOrderList détermine l'ordre de traitement des fichiers à proprement parler. Par exemple, dans la configuration par défaut, tous les fichiers jQuery sont toujours regroupés avant les fichiers Modernizr.

L'impact de la classe DefaultBundleOrderer sur les fichiers CSS est plus limité. Si votre site Web contient un fichier nommé « reset.css » ou « normalize.css », ces fichiers seront automatiquement regroupés avant tout autre fichier CSS, et reset.css précédera toujours normalize.css. Pour ceux qui ne connaissent pas les fichiers de style reset/normalize, l'objectif de fichiers de style de ce type est de vous procurer un ensemble standard d'attributs de style pour tous les éléments HTML, afin que votre page n'hérite pas de paramètres spécifiques au navigateur, tels que les polices, les tailles et les marges. Même s'il existe des recommandations concernant le contenu de ces deux types de fichiers, il vous revient entièrement de décider ce que vous y mettez. Si vous avez des fichiers portant ces noms dans votre projet, alors votre ASP.NET MVC 4 fait un effort supplémentaire pour s'assurer qu'ils sont regroupés avant tout autre élément. Si vous souhaitez ignorer la propriété Orderer par défaut et tout ordre prédéfini de regroupement des fichiers, vous avez deux options : Premièrement, vous pouvez créer votre propre fichier de définition de l'ordre, fonctionnant regroupement par regroupement. Voici un exemple permettant de simplement ignorer l'ordre prédéfini :

public class PoorManOrderer : IBundleOrderer
{
  public IEnumerable<FileInfo> OrderFiles(
    BundleContext context, IEnumerable<FileInfo> files)
  {
     return files;
  }
}

Vous pouvez l'utiliser ainsi :

var bundle = new Bundle("~/mycss");
bundle.Orderer = new PoorManOrderer();

De plus, vous pouvez réinitialiser tous les ordres prédéfinis à l'aide de ce code :

bundles.ResetAll();

Dans ce cas, l'effet de l'utilisation de la propriété Orderer par défaut (le choix du pauvre) illustré ci-dessus est le même. Remarquez, cependant, que ResetAll réinitialise également l'ordre des scripts.

La deuxième fonctionnalité, qui est plus poussée, est la liste d'éléments à ignorer. Via la propriété IgnoreList de la classe BundleCollection, elle permet de définir des chaînes correspondant à un modèle et sélectionnées pour être prise en compte mais que vous souhaitez ignorer. Le principal avantage de l'implémentation de regroupements via ASP.NET MVC 4 est de traiter tous les fichiers JavaScript (*.js) du dossier en un seul appel. Cependant, il est probable qu'avec *.js vous appeliez aussi des fichiers qui n'ont pas à être téléchargés, tels que des fichiers vsdoc.js. La configuration par défaut d'IgnoreList se charge des scénarios les plus courants tout en vous donnant la possibilité de personnaliser certains aspects.

Regroupement CSS en action

Le regroupement CSS est une fonctionnalité d'optimisation performante, mais comment fonctionne-t-elle en pratique ? Examinons le code suivant :

var bundle = new Bundle("~/mycss");
bundle.Include("~/content/styles/*.css");           
bundles.Add(bundle);

Le trafic HTTP correspondant est illustré à la figure 2.

The Second Request Is a Bundle with Multiple CSS Files
Figure 2 La deuxième requête est un regroupement comportant plusieurs fichiers CSS

La première requête correspond à la page d'accueil, la deuxième ne pointe pas vers un fichier CSS spécifique mais fait référence à un regroupement qui contient tous les fichiers CSS d'un dossier de contenu/styles (voir la figure 3).

An Example of Bundled CSS
Figure 3 Exemple de regroupement CSS

Ajout de la minimisation

La classe Bundle ne s'occupe que de regrouper plusieurs ressources ensemble afin qu'elles soient englobées dans un téléchargement unique et mises en cache.

Comme vous pouvez le constater dans le code de la figure 3, le contenu à télécharger est cependant agrémenté d'espaces et de retours à la ligne afin d'être plus facilement lisible. Les navigateurs, en revanche, n'ont que faire de ces caractères, et, pour eux, le code CSS de la figure 3 est entièrement équivalent au code de cette chaîne minimisée :

html,body{font-family:'segoe ui';font-size:1.5em;margin:10px}
  html,body{background-color:#111;color:#48d1cc}

Dans le cas de cet exemple, le code CSS étant très simple, l'impact de la minimisation est limité. Cependant, la minimisation CSS prend tout son sens dans les cas de sites professionnels comportant des fichiers de style très volumineux.

Comment ajouteriez-vous la minimisation ? Il suffit simplement de remplacer la classe Bundle par la classe StyleBundle. StyleBundle est véritablement très simple à utiliser. Elle hérite de Bundle et consiste en un autre constructeur :

public StyleBundle(string virtualPath)
  : base(virtualPath, new IBundleTransform[] { new CssMinify() })
{
}

La classe Bundle dispose d'un constructeur qui accepte une liste d'objets IBundleTransform. Ils sont appliqués les uns après les autres au contenu. La classe StyleBundle se contente d'ajouter le transformateur CssMinify. CssMinify est la minimisation par défaut pour ASP.NET MVC 4 (et les versions ultérieures) et se base sur les outils d'optimisation WebGrease (webgrease.codeplex.com). Inutile de dire que si vous souhaitez passer à une autre minimisation, tout ce que vous avez à faire est d'obtenir la classe (une implémentation de IBundleTransform) et de la transmettre via le constructeur de la classe Bundle.

Plus d'excuses

L'infrastructure d'optimisation Web regroupée via ASP.NET MVC 4 ajoute un tout petit morceau de fonctionnalité, mais il s'agit d'une fonctionnalité dont il est préférable de ne pas se passer. Il n'y a tout simplement aucune raison de ne pas utiliser la minimisation et le regroupement des ressources. Jusqu'à présent, on pouvait dire que l'absence de prise en charge native dans la plate-forme ASP.NET était une excuse. Mais ce n'est plus vrai avec ASP.NET MVC 4 et les versions plus récentes.

En ce qui concerne les fichiers CSS, il y a un autre aspect à prendre en compte: la génération dynamique des styles. Conçu pour n'être pas plus qu'une apparence donnée aux pages, CSS devient une ressource de plus en plus dynamique, à tel point que des pseudo-langages de programmation ont été inventés pour générer du CSS par programme. C'est ce dont je vous parlerai dans mon prochain article.

Dino Esposito est l'auteur de « Architecting Mobile Solutions for the Enterprise » (Microsoft Press, 2012) et de « Programming ASP.NET MVC 5 » à venir (Microsoft Press). Développeur technique pour les plateformes .NET et Android chez JetBrains, Dino Esposito participe régulièrement aux différents événements de par le monde et partage sa vision du logiciel sur software2cents.wordpress.com et sur Twitter : twitter.com/despos.

Merci à l'expert technique suivant d'avoir relu cet article : Christopher Bennage (Microsoft)
Christopher Bennage est développeur chez Microsoft au sein de l'équipe Patterns & Practices (Modèles et pratiques). Son travail consiste à découvrir, collecter et encourager les pratiques qui réjouissent les développeurs. JavaScript et le développement de jeux (informels) figurent parmi ses centres d'intérêt actuels. Il tient un blog dont l'adresse est dev.bennage.com.