Optimiser ses resources web statiques avec wro4j
****
La rapidité de chargement d’une page web dépend essentiellement du temps qu’il faut au navigateur pour en télécharger les données (c.f. la très bonne collection d’articles d’optimisation de yahoo); La manière la plus efficace et évidente d’optimiser le chargement d’une page est de réduire au maximum les requêtes vers les ressources statiques : css, javascript, et images. Pour les images, la solution de la feuille de sprite reste la plus valable pour des raison de compatibilité (pour ce faire, je recommande spritecow) mais il y a une alternative plus “moderne” que j’évoquerai plus tard. Pour les feuilles de style et les script, la problématique est un peu plus complexe…
l’optimisation du chargement des scripts et des feuilles de style.
Il y a 2 axes principaux d’optimisation. Le premier est la compression des scripts pour en réduire la taille et ainsi en accélérer le chargement, le second, est de limiter le nombre de requêtes en fusionnant les ressources. Un moyen simple de vérifier si vos appels de scripts et de css sont optimisés, est de lancer un diagnostique à l’aide de l’outil de développement de chrome (onglet “audit”, puis “run”), ou encore à l’aide de YSlow.
La compression des javascripts et css se résume en fait à la suppression des espaces et retours à la ligne inutiles ainsi que des commentaires. Pour ce faire, il existe de nombreux scripts connus de compression : YUI compressor, Google Closure, JSMin, Csslint… Mais tous ne se chargent pas de fusionner les fichier ensemble. On peut alors faire en plus appel à des script de merge comme JMerge. Bien évidemment, une fois compressé et fusionné, les .css et .js deviennent illisibles.
La meilleure pratique dans un projet est donc de conserver ces éléments décompressés dans ses sources pour pouvoir développer sereinement, et se charger de la compression lors du build. Et c’est là que le bat blesse. Avec les outils cités précédemment il faut soit compresser son code “manuellement”, soit lancer un script d’exécution lors du build, en shell ou via son outil de build (maven). Rien d’insurmontable, mais pas la panacée en terme de maintenabilité et de stabilité. Si l’on ajoute à cela l’utilisation d’extensions commes Sass ou Coffeescript, cela peut vite devenir un casse tête. C’est là qu’intervient wro4j.
Wro4j : une solution java intégrée.
Présentation
C’est en cherchant une manière facile d’integrer les fusions et compressions de ces ressources à mon build maven que je suis tombé sur le projet java wro4j. Il se charge, soit à l’exécution via un Filter, soit au build via un plugin Maven, de compresser et fusionner vos ressources. En plus d’offrir la possibilité d’utiliser les algorithmes de compression de votre choix (YUI compressor, Google Closure, JSMin, JSlint…), il est très facilement extensible et configurable.
Wro4j opère de la manière suivante : vous définissez des groupes de fichier js et css, et au lieu d’appeler directement les ressources, vous appeler le nom du groupe. A l’exécution le Filter va se charger de constituer vos ressources compressé, et vous renvoyer une seule ressource compacte. Si l’on choisit de faire cela au build, les ressources compressées sont bien évidemment pré générées. Notez que la définition de groupes vous force, à organiser vos resources.
Utilisation
Je peux donc au choix mettre en place wro4j via un filter dans mon web.xml
Ou en mode build maven dans mon pom.xml
ro.isdc.wro4j wro4j-maven-plugin ${wro4j.version} compile run all ${basedir}/src/main/webapp/wro/ ${basedir}/src/main/webapp/
Maintenant admettons que j’utilise un moteur de template pour mon application web et que le template principal contienne les inclusions suivantes :
Comme ces inclusions sont à priori communes à toutes les pages du site, l’idéal serait donc de regrouper ces fichiers. Pour cela je définis le groupe de ressources suivant en xml dans un fichier que je fourni à wro4j (par défaut /WEB-INF/wro.xml) :
/static/css/style.css /static/css/global/*.css /static/js/jquery/jquery.js /static/js/jquery/jquery.ui.js /static/js/**
NB : vous remarquerez la possibilité d’utiliser des wildcards.
Wro4j se charge ensuite de compresser et de fusionner les fichiers en fonction des groupes que l’on a défini, au build, ou à la volée si l’on utilise le filter. Pour appeler les ressources du groupe “common” définit précédemment dans nos pages web :
Mode opératoire
La classe centrale de wro4j est WroManager. Elle offre un ensemble de possibilités de personnalisation simple. Le WroManager opère de la manière suivante : (cf : http://wro4j.googlecode.com/svn/wiki/img/wro4j-process.png)
1) Construire le modèle de données
Le WroModel contient les informations de regroupement des fichiers, et va permettre leur fusion par la suite. Il est construit par une WroModelFactory, capable de lire ces information sous forme :
- de xml (cf l’exemple précédent)
- de groovy
- de json
Par défaut, la SmartWroModelFactory est appelée. Elle va essayer de trouver les informations sous ces 3 formats successivement. Il est également très simple de faire sa propre WroModelFactory.
2) Localisation des ressources à traiter à l’aide d’une classe Locator
3) Pre-traitement des ressources.
Par exemple compilation de coffeescript, ou Sass, mais surtout réécriture des urls des images dans le css, et la minification des ressources.
4) Fusion des ressources.
5) Post-traitement des ressources.
Les classes et interfaces utilisées pour la construction du modèle et la localisation des ressources sont facilement extensible/implémentable, comme nous le verrons dans l’exemple suivant. Pour les étapes de pre et post-processing, wro4j offre une configuration par défaut, mais il existe de nombreux processors disponibles. A noter que ces processors sont également utilisables de manière autonome.
Un exemple de “customisation”
A titre d’exemple, je vais vous faire un retour d’expérience : j’ai souhaité faire la déclaration des groupes wro4j directement dans un fichier freemarker – le framework de templating que j’utilisais alors. Le fichier de templating contenant les informations de groupe freemarker, cela lui permet de construire du html appelant soit les ressources décompressées et non fusionnées, soit les ressources assemblées par wro4j (au gré d’un flag dans la configuration du serveur par exemple).
Comme on ne va pas dupliquer ces informations, il faut permettre à wro4j de les lire.
Pour cela, Il m’a suffit d’implémenter une WroModelFactory (cf étape 1), qui construit le modèle de groupes wro4j (WroModel) à partir des données stockées dans le fichier freemarker :
public class CustomWroModelFactory implements WroModelFactory { private static final String PACK_EXT = ".pack"; /** * method create, appelée par le framework wro4j * renvoie un model wro. / @Override public WroModel create() { ParsedResourceContainer container = parseModel(); WroModel model = new WroModel(); for (CustomWroModelFactory.ParsedResourcesGroups rgroup : container.resources_groups) { Group group = new Group(rgroup.name + PACK_EXT); for (String cssFilePath : rgroup.resources.css) { group.addResource(Resource.create(cssFilePath,ResourceType.CSS)); } for (String jsFilePath : rgroup.resources.js) { group.addResource(Resource.create(jsFilePath,ResourceType.JS)); } model.addGroup(group); } return model; } /* * va parser la variable resources dans mon fichier freemarker * à l’aide de (l’indispensable) Gson http://code.google.com/p/google-gson/ / public ParsedResourceContainer parseModel() { // } /* *Un bean pour stocker les données parsée par Gson */ private class ParsedResourceContainer{ // } }
NB : Comme les variables freemarker on un format proche du Json j’ai utilisé Gson pour parser.
Puis il suffit de faire un CustomWroManager factory qui va appeller CustomWroModelFactory :
/** * Factory pour un wro manager (objet 'central' de wro4j) * surchargeant la méthode newModelFactory afin de renvoyer * la CustomWroModelFactory */ public class CustomWroManagerFactory extends DefaultStandaloneContextAwareManagerFactory { @Override protected WroModelFactory newModelFactory() { return new CustomWroModelFactory(); } }
Il ne reste qu’a configurer le build maven en lui spécifiant la WroManagerFactory à utiliser :
ro.isdc.wro4j wro4j-maven-plugin 1.4.1 compile run webcore.buildhelper.CustomWroManagerFactory true ${project.build.directory}/${project.build.finalName}/wro/ ${basedir}/src/main/webapp/
…pour que les fichiers compressés/fusionnés soient créés lors du build, à partir des données de mon fichier freemarker.
Conclusion
Il existe des solutions similaires à wro4j, telles que Jawr (dont wro4j s’est surement inspiré), mais j’ai choisi de l’utiliser car :
- Il est très bien documenté.
- L’activité du groupe google-code wro4j est importante.
- J’avais un bug – non bloquant – sur un cas particulier de réécriture des urls des images css, je l’ai signalé au groupe google de wro4j et il a été corrigé dans la release suivante.
- Les releases sont fréquentes, et ajoute de nouvelles fonctionalités.
- Il gère un nombre varié d’algorithmes de compression et de traitement des ressources (je pense notamment à à des “langages” comme CoffeScript ou Sass).
Enfin, je mentionnais en introduction qu’il existe une autre manière d’optimiser le chargement des images que les feuilles de sprite : il s’agit des data URIs, un type d’uri de plus en plus utilisé qui plutôt que de donner une référence à une donnée via une url, donne directement cette donnée encodée en Base64. Et bien wro4j gère également la conversion d’images en base64 dans vos css.
Si vous avez affaire à des problématiques d’optimisation des ressources statiques, je vous recommande donc de jeter un oeil sur wro4j. J’ai été vraiment impressionné par son adaptabilité, son extensibilité et sa simplicité d’utilisation.