Optimiser le temps de chargement d'une application GWT (1/2)

le 23/07/2009 par Rudy Krol
Tags: Software Engineering

Le temps de chargement d'une application informatique est un point essentiel en terme d'usabilité. Il a un impact important sur l'expérience utilisateur, tellement important qu'il peut être le facteur décisif d'adhésion ou de rejet de l'application par les utilisateurs qui se font un avis en 2-3 secondes. On a tous des exemples douloureux en tête... ou pas d'ailleurs... et c'est bien ça le drame : ces applications passent aux oubliettes! Les site web sont évidemment très concernés par cette problématique, la concurrence est rude sur la toile… et les plus performants marquent des points face à leurs concurrents, des points qui valent très cher! Les grands du web (Google, Yahoo, etc.) l’ont bien compris et en ont fait leur priorité n°1.

Le problème : le temps de chargement d’une page web dépend de la richesse du contenu et des fonctionnalités qu’elle offre… autant de choses qui sont de plus en plus attendues par les utilisateurs.

Pour résumer, quand on construit une application internet riche (RIA), on est de plus en plus confronté à la question suivante :

Comment concilier performance et richesse fonctionnelle ?

Le temps de chargement d'une RIA est la somme de deux phases :

  • le téléchargement des composants web : la page hôte, l'application (JS, SWF ou autre) et autres fichiers CSS, images, vidéos, etc.
  • l'initialisation/instanciation de la vue dans le browser

Si on compare ces deux phases entre une application web classique (j’utilise ce terme pour désigner les applications web dont le contenu HTML est généré côté serveur) et une application RIA (utilisant les nouveaux paradigmes consistant à générer les vues côté client), on obtient (grosso-modo... c'est l'ordre de grandeur qui nous intéresse ici) les graphiques suivants :

graphique

J'ai volontairement séparé le chargement de la vue initiale et les vues suivantes pour faire apparaître les spécificités des RIA en terme de chargement. En effet, le chargement initial d'une RIA est souvent douloureux comparé aux application web classique, par contre le chargement des vues suivantes est globalement plus réactifs pour une RIA. Cet article est donc découpé en deux, correspondant à ces deux grandes phases de chargement, avec comme objectif de :

  • rassembler les bonnes pratiques permettant d'optimiser les temps de téléchargement et de les illustrer au travers d’une application Google Web Toolkit (GWT)
  • aborder les différentes approches d'architectures permettant d'optimiser les temps d'initialisation d'une application GWT sur le navigateur

Optimiser le temps de téléchargement des composants web

Respecter les pratiques standards : compresser, mettre en cache, déclarer proprement

Les "bibles" des bonnes pratiques de développement web de Google et Yahoo sont disponibles en ligne :

Les règles les plus classiques liées à l'optimisation du téléchargement des composants web sont essentiellement :

  • optimiser la taille des composants téléchargés : compression des fichiers multimédia (images, vidéos), optimisation des fichiers css (ne laisser que les styles qui surchargent les styles par défaut ou les styles hérités), compression des composants (exemple : mod_gzip sur Apache HTTP Server)
  • paramétrer la bonne politique de cache : mettre en cache browser l'application afin d'éviter de retélécharger l'application à chaque visite (attention toutefois à ne pas garder en cache le fichier "*nocache.js" généré par GWT)
  • déclarer les CSS dans la section HEAD du fichier HTML et déclarer les fichiers Javascript en bas de page HTML

Une fois que tout ça est respecté, reste à optimiser l'application elle-même...

Optimiser la taille d'une application GWT (i.e. des fichiers Javascript générés)

La taille d'une application GWT est dépendante de la quantité de code "Java client" produit et du nombre de composants utilisés (DatePicker, Panels, etc.). Même si GWT optimise le code Javascript généré (suppression du code mort, génération d'un fichier Javascript par type et version de navigateur et par langue pour éviter un gros fichier Javacript prenant en compte toutes les spécificités de chaque navigateur et langue, etc.), un premier niveau d'optimisation sera de :

  • factoriser au maximum le code "Java client" à l'aide de widgets réutilisables, idéalement en héritant de la classe Composite, les bonnes pratiques sont disponibles sur le blog officiel GWT
  • obfuscer le code de l'application en définissant l'argument de la commande de compilation GWT "-style OBF" (activé par défaut). L'intérêt est de réduire au maximum la taille du fichier Javascript généré en utilisant des noms de classes/méthodes/variables les plus courts possibles.
  • éviter dès que possible l'utilisation de bibliothèque de composant tierces type Wrapper Javascript (ex : GWT-Ext, Tatami, etc.) qui nécessiteront le téléchargement de nouveaux fichiers Javascript, non optimisés par la compilation GWT. Une autre bonne raison de ne pas les utiliser est de ne pas provoquer de fuite mémoire en court-circuitant les mécanismes de GWT qui garantissent la libération des objets non utilisés. De manière générale, j'ai tendance à préférer implémenter moi-même mes widgets plutôt que d'utiliser les librairies tierces dont on peut avoir quelques doutes sur la stabilité et la pérennité. Un autre intérêt est de garder la maîtrise du comportement des composants qui doivent souvent être customisés pour répondre aux besoins métier.

A ce titre, GWT 2.0 viendra avec son lot de nouveautés et notamment la possibilité de générer des rapports Story Of Your Compile (SOYC) permettant de mieux cibler les portions de code à optimiser. Ca fera l'objet d'un autre article en préparation...

Diminuer le nombre de composants à télécharger

Pour palier à la contrainte HTTP1.1 des 2 connexions HTTP simultanées sur le même domaine, il est indispensable de diminuer au maximum le nombre de composants qui seront téléchargés :

  • regrouper dès que possible les styles dans un seul fichier CSS générique
  • ajouter au maximim 1 fichier CSS spécifique à la vue pour éviter un trop gros fichier CSS générique
  • pour les mêmes raisons que précédement, utiliser des librairies tierces necessitera de télécharger des ressources (CSS, images, etc.) trés riches, donc lourds, et pas toujours découpés par composants graphiques. De plus ces ressources ne seront pas mis dans un fichier Sprites (mécanisme décrit ci-dessous)
  • utiliser la technique CSS Sprites permettant de télécharger plusieurs icônes en une seule requête. Le fonctionnement est auto-magique, les icônes sont fusionnés dans un seul fichier image, puis positionné sur le navigateur de sorte à ne faire apparaître que l'icône concernée. L'utilisation de cette technique en GWT est "transparente" à travers ImageBundle. Pour donner un exemple concret :
public interface Icons extends ImageBundle {
    public static final Icons INSTANCE =  GWT.create(Icons.class);
    @Resource("com/octo/sample/public/images/logo.gif")
    public AbstractImagePrototype getLogo();
    @Resource("com/octo/sample/public/images/info.png")
    public AbstractImagePrototype getInfo();
}

Dans l'exemple ci-dessus, les deux icônes logo et info sont automatiquement fusionnés à la compilation GWT dans le même fichier image. Il suffit d'ajouter ces images dans l'interface graphique de la manière suivante par exemple :

panel.add(Icons.INSTANCE.getInfo())

GWT va générer le code permettant de positionner l'image en CSS de sorte à faire apparaître uniquement l'icône info.

En GWT 2.0, le concept de ImageBundle va être étendu aux autres ressources envoyées côté client (CSS, XML, properties, etc.) via le ClientBundle. Les fichiers ressources seront donc fusionnés au moment de la compilation, voire intégrées au fichier Javascript et mis en cache côté client (il y a d'autres intérêts mais qui concernent moins le sujet de cet article).

Modulariser l'application

Une autre grosse nouveauté GWT 2.0, appelée CodeSplitting, permettra de découper l'application en plusieurs modules, c'est à dire plusieurs fichiers Javascript. Cela permettra d'éviter un seul gros fichier Javascript, long à télécharger au démarrage de  l'application sur le client.

L'objectif de cette fonctionnalité est donc clairement de diminuer le temps de téléchargement au chargement de l'application sur le client et tendre vers les proportions suivantes :

graphique_modularisation

L'utilisation de cette fonctionnalité est très simple, il suffit d'utiliser l'API GWT.runAsync comme suit :

GWT.runAsync(new RunAsyncCallback() {
    public void onFailure(Throwable reason) {
    }
    public void onSuccess() {
        SearchView searchView = new SearchView ();
    }
});

Dans l'exemple ci-dessus, la déclaration et l'instanciation d'un composant (ici une vue de type SearchView) sont encapsulées dans la méthode onSuccess d'une classe anonyme de type RunAsyncCallback, en paramètre de la méthode static runAsync de la classe GWT. Dans cet exemple, le code Javascript correspondant à la classe SearchView, et tous les composants qu'il utilise de manière exclusive (à l'exception des composants issus de librairies tierces type Wrapper JS) sera stocké dans un fichier Javascript séparé du fichier Javascript général. Ce fichier sera automatiquement téléchargé sur le client à l'exécution, l'idéal étant de déclarer ce code sur traitement d'un évènement non déclenché à l'initialisation de la RIA (lors du traitement de l'évènement clic sur le menu "Search" pour reprendre l'exemple).

Pour être plus précis, GWT détecte à la compilation que le composant SearchView est candidat à un SplitPoint puisqu'il n'est utilisé qu'à travers un seul runAsync. Par contre, les composants utilisés dans plusieurs SplitPoints sont stockés dans un autre fichier Javascript partagé mais différent du fichier Javascript du chargement initial, en gros ça donne le découpage suivant :

  • 1 fichier Javascript pour le code de la vue initial
  • n fichiers Javascript correspondants aux SplitPoints
  • 1 fichier Javascript contenant le code qui n'est ni spécifique au chargement initial, ni spécifique à un unique SplitPoint

Vous l'aurez compris, ce mécanisme n'est pas forcément simple à utiliser et j'imagine que vous vous posez déjà quelques questions comme :

  • comment découper au mieux mon application ? Je tenterai d'apporter une réponse dans l'article suivant.
  • comment savoir quel code se retrouve dans quel fichier Javascript ? J'ai un article en préparation sur l'outil SOYC qui semble répondre au besoin.

On a donc vu dans cet article comment optimiser les temps de téléchargement d'une application GWT et des composants web sur le client, l'article suivant concernera l'optimisation du temps d'initialisation d'une application GWT sur le browser... stay tuned! ;)