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

<filter>
    <filter-name>WebResourceOptimizer</filter-name>
    <filter-class>ro.isdc.wro.http.WroFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>WebResourceOptimizer</filter-name>
    <url-pattern>/wro/*</url-pattern>
</filter-mapping>

Ou en mode build maven dans mon pom.xml

<plugins>
    <plugin>
        <groupId>ro.isdc.wro4j</groupId>
        <artifactId>wro4j-maven-plugin</artifactId>
        <version>${wro4j.version}</version>
        <executions>
                <execution>
                <phase>compile</phase>
                <goals>
                    <goal>run</goal>
                </goals>
             </execution>
        </executions>
        <configuration>
            <targetGroups>all</targetGroups>
            <destinationFolder>${basedir}/src/main/webapp/wro/</destinationFolder>
            <contextFolder>${basedir}/src/main/webapp/</contextFolder>
        </configuration>
    </plugin>
</plugins>

Maintenant admettons que j’utilise un moteur de template pour mon application web et que le template principal contienne les inclusions suivantes :

<!-- début de page -->
<link rel="stylesheet" type="text/css" href="/static/css/style.css" />
<link rel="stylesheet" type="text/css" href="/static/css/global/picto.css" />
<link rel="stylesheet" type="text/css" href="/static/css/global/header.css" />
<link rel="stylesheet" type="text/css" href="/static/css/global/footer.css" />
<link rel="stylesheet" type="text/css" href="/static/css/global/divers.css" />
<!-- fin de page -->
<script type="text/javascript" src="/static/js/jquery/jquery.js"></script>
<script type="text/javascript" src="/static/js/jquery/jquery.ui.js"></script>
<script type="text/javascript" src="/static/js/common.js"></script>
<script type="text/javascript" src="/static/js/mustache.js"></script>
<script type="text/javascript" src="/static/js/iCanHaz.js"></script>

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

<groups xmlns="http://www.isdc.ro/wro">
    <group name="common">
        <css>/static/css/style.css</css>
        <css>/static/css/global/*.css</css>
        <js>/static/js/jquery/jquery.js</js>
        <js>/static/js/jquery/jquery.ui.js</js>
        <js>/static/js/**</js>
    </group>
</groups>

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 :

<link rel="stylesheet" type="text/css" href="/wro/common.css" />
<script type="text/javascript" src="/wro/common.js"></script>

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 :

<plugin>
    <groupId>ro.isdc.wro4j</groupId>
    <artifactId>wro4j-maven-plugin</artifactId>
    <version>1.4.1</version>
    <executions>
        <execution>
            <phase>compile</phase>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
          <wroManagerFactory>webcore.buildhelper.CustomWroManagerFactory</wroManagerFactory>
          <minimize>true</minimize>
          <destinationFolder>${project.build.directory}/${project.build.finalName}/wro/</destinationFolder>
          <contextFolder>${basedir}/src/main/webapp/</contextFolder>
    </configuration>
</plugin>

…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.

TwitterFacebookGoogle+LinkedIn
  • Harold Capitaine

    Merci pour cet article.
    Cela faisait un moment que je cherchais ce type de librairie, simple mais customisable tout de même.
    En plus ce projet semble être réellement actif.
    Je vais tester cela dès que possible

  • http://twitter.com/hikagebe Gildas Cuisinier

    Si je comprends l’intérêt, est-ce que cela ne risque pas de réduire la productivité lors des développements ?

    Remplacer :

    Par :

    va avoir comme conséquence de ne plus être gérer par l’IDE, et donc perdre tout l’autocomplétion, etc non ?

    • Kévin VASSEURE

      je ne suis pas sûr de comprendre. En fait les ressources ne sont compressé qu’au build, donc vous travaillez sur vos fichiers décompressés dans votre IDE, et disposerez de l’autocompletion des css et des js. En revanche, c’est vrai que vos ressources seront compressées lorsque vous lancerez votre serveur en local. Ce n’est pas un problème pour les css, puisque les firebugs ou autre inspecteurs vous présentent ces données misent en forme; en revanche, pour faire du débug de javascript dans votre navigateur, c’est un peu plus problématique. C’est pour cela que j’ai mis en place la solution que j’expose dans mon deuxième exemple.

      • http://twitter.com/hikagebe Gildas Cuisinier

        Effectivement concernant l’édition directement dans les scripts, cela ne changera rien.

        Par contre, si j’utilise des fonctions des scripts dans la page html, vu que le fichier “script” importé ne sera généré qu’au runtime, la je n’aurais plus d’autocomplétion.

        • Kévin VASSEURE

          Ha oui je comprend mieux votre problème. Cela dit, si vous passez par le plugin maven de wro4j, vous pouvez le configurer pour qu’il génère les fichiers compressés dans vos sources, plutôt que dans votre livrable, et ainsi l’autocomplétion devrait fonctionner.

  • http://www.droff.com/ Francois Le Droff

    une autre solution intéressante dans ce domaine : 
    https://github.com/jakewins/brew