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

le 11/11/2009 par Rudy Krol
Tags: Software Engineering

La première partie de cet article a permis d’introduire la problématique de chargement des RIA, en commençant par expliquer comment optimiser le temps de téléchargement d’une application web basée sur GWT, notamment à travers la modularisation. Cette deuxième partie aborde l’optimisation du temps d’initialisation d’une application sur le browser, toujours illustré à travers la technologie GWT.

Optimiser le temps d'initialisation d'une application GWT

Optimiser les communications client/serveur

Une des premières étapes de l’initialisation d’une RIA consiste à charger des données à travers une ou plusieurs requêtes au serveur. Ces données sont requises à l’initialisation, soit parce qu’elle sont affichées sur la vue initiale, soit parce qu’elles participent aux traitements déportés côté client. Les données qui transitent entre le client et le serveur doivent donc correspondre, ni plus ni moins, à ces besoins. En effet, l’objectif est de n’envoyer au client que les données réellement utilisées pour optimiser aussi bien le volume de données téléchargées que le temps de sérialisation/désérialisation des objets, quelque soit le protocole utilisé. Pour cela, un des pièges à éviter selon moi est d’exposer, côté serveur, des services qui renvoient directement le modèle d’entités persistantes. Même si, techniquement, cette solution est aujourd’hui rendue possible par l’utilisation du framework Gilead, elle impose un modèle souvent inadapté aux besoins du client (en dehors des applications de type CRUD) d'un point de vue fonctionnel et limité en terme d’optimisation. En effet, le mécanisme de chargement des collections à la demande (lazy/eager) peut être exposé au niveau de l’API serveur mais s’avère souvent insuffisant lorsqu’il est question de réduire le nombre de requêtes et d’optimiser les temps de réponse.

L’utilisation d’un modèle DTO (Data Transfer Object) devient alors souvent incontournable, même si cette méthode est moins productive que Gilead puisqu’elle impose l’implémentation du mapping entre les deux modèles (DTO <=> persistant). Un moyen simple d’obtenir une visibilité sur les requêtes serveur avec leur temps de réponses ainsi que les temps de sérialisation/déserialisation est d’utiliser le DebugPanel. Ce composant graphique s’utilise durant les phases de développement en s’insérant dans la même page web que l'application GWT et offre un certain nombre d’indicateurs indispensables pour contrôler précisément ce qui passe entre le client et le serveur.

Profiter pleinement du caractère asynchrone de la technologie GWT

Les moteurs Javascript sont aujourd’hui mono-thread, autrement dit les browsers ne peuvent pas exécuter plusieurs traitements en parallèle. Ajax permet de simuler un comportement multi-thread en offrant la possibilité de paralléliser les  traitements entre le client et le serveur à travers des requêtes asynchrones. Comme on l’a évoqué précédemment, le chargement d’une page nécessite souvent une sollicitation du serveur pour récupérer les données à afficher. Le client doit également instancier un certain nombre de composants. Un axe d’optimisation est donc de paralléliser recherche des données sur le serveur et instanciation des composants de la vue sur le client. Concrètement, voici une comparaison entre deux séquences de chargement, l’une ne profitant pas de la parallélisation apportée par l’asynchronisme, l’autre oui :

diag_seq

Une autre bonne pratique consiste à afficher les informations de manière progressive sur la vue initiale, suivant un découpage fonctionnel. En effet, certaines RIA affichent la vue d’accueil une fois qu’elle est entièrement initialisée, à la manière des sites web classiques… Le temps d’attente peut devenir assez frustrant pour l’utilisateur, surtout si finalement l’information qui intéresse le plus l’utilisateur aurait pu être affichée très rapidement! L’idée est donc d’afficher le plus tôt possible les fonctionnalités pour lesquels l’utilisateur ouvre l’application. L’exemple le plus parlant qui me vient en tête est l’affichage des mails dans la vue initiale de GMail, même si le panneau « Chat » n’est pas encore chargé.

Pour aller plus loin, un bloc fonctionnel peut lui-même être très lourd à charger entièrement. Ce cas de figure arrive fréquemment lorsqu’on souhaite faire apparaître une liste ou un tableau volumineux. Pour rester sur l’exemple Google, mais cette fois-ci avec l’application Wave, on peut remarquer que les Waves ne sont pas toutes affichées dès l’apparition de la page d’accueil. Elles apparaissent de manière progressive, ce qui permet à l’utilisateur de pouvoir très rapidement consulter les Waves qui se trouvent en haut de la liste (les plus récentes) sans devoir attendre que la liste soit entièrement remplie. En GWT, l’API DefferedCommand facilite l’implémentation de ce chargement progressif. En fait, Javascript étant mono-thread, GWT traite l’asynchronisme à travers une pile de commandes. Le fait d’implémenter une DefferedCommand permet d’ajouter une commande à cette pile. Cette commande est alors traitée lorsque toutes les commandes préalablement ajoutées à la pile ont été exécutées. Voici un exemple d’utilisation de l’API permettant de charger un tableau volumineux de manière progressive :

//on charge les 5 premières lignes du tableau tout de suite
loadTableLines(0, 4);
//on charge le reste du tableau en différé par lots de 5 lignes
for(int i = 5 ; i < list.length() ; i+5){
    DeferredCommand.add(new Command() {
        public void execute() {
            loadTableLines(i, i+4);
        }
    });
}

Du chargement à la demande (ou « lazy loading »)...

Il existe différentes approches permettant de dessiner l’architecture d’une GUI, les principales étant MVC, HMVC et MVP. Ces architectures permettent de découper l’application en composants (MVC ou MVP) améliorant la modularité de l'application, et donc sa maintenabilité et sa réutilisabilité. Concrètement, prenons l’exemple d’une application ressemblant à ce schéma classique :

schema_pages

Pour ce type d’application, et sans rentrer dans le détail des widgets composants les vues ni dans le détail des classes du modèle, on peut imaginer un découpage MVP ressemblant à celui-ci :

decoupage_mvp

Pour afficher la vue d’accueil, il est donc nécessaire d’instancier 3 composants : le menu, l’en-tête et le contenu initial, c’est à dire la liste des comptes dans le cas présent. Dans une démarche d’optimisation du temps d’initialisation d’une application GWT, il est important de toujours bien penser à instancier les composants seulement au moment où l’utilisateur souhaite les afficher. En Flex, cela se fait automatiquement pour beaucoup de composant avec une policy par défaut qui est d'initialiser les composants uniquement lorsqu'ils deviennent visibles pour la première fois (voir la doc sur la propriété creationPolicy pour plus d’informations). Ce n’est pas le cas pour GWT où le chargement à la demande doit être implémenté par du code spécifique ou à l'aide du composant LazyPanel ou par le framework MVC/MVP utilisé. Dans cet exemple, les classes View et Presenter des composants ShowAccount et EditAccount ne doivent donc pas être instanciés à l’initialisation de l’application, mais uniquement lorsque l’utilisateur souhaitera naviguer sur une de ces sections via le menu. Cette pratique peut paraître logique et triviale, mais tous les frameworks MVC/MVP ne le gèrent pas. On peut par exemple citer le framework mvp4g qui instancie tous les composants dès l’initialisation, contrairement à la version MultiCore de PureMVC. Attention donc à bien prendre en compte ce critère pour le choix d'un framework d’architecture de client GWT.

... au téléchargement à la demande!

Une fois ce type d’architecture mis en place, les triades deviennent un support idéal au CodeSplitting décrit dans l’article précédent. Il suffit alors d’instancier les différents composants à travers l’API GWT.runAsync(RunAsyncCallback callback) pour mettre en œuvre non plus un chargement à la demande mais un véritable « téléchargement à la demande », permettant de modulariser l’application de manière optimale. En effet, la granularité apportée par ce découpage en triades MVC/MVP permet de minimiser le nombre de téléchargements de modules avec un seul  téléchargement maximum par vue affichée. Pour reprendre l’exemple précédent, on obtiendrait le découpage en modules suivant :

decoupage_mvp_modules

Dans l’idéal, il serait même encore plus intéressant de regrouper plusieurs composants dans un même module en fonction de la fréquence d’utilisation des fonctionnalités par les utilisateurs. Pour reprendre l’exemple d’un client mail, on peut regrouper l’affichage des mails et la vue permettant de rédiger un mail dans le même module, et exclure la vue « Settings » dans un module dédié, car utilisé beaucoup plus occasionnellement. On combine alors téléchargement à la demande des modules et chargement à la demande des composants du module.

Enfin, tout dépend du type de client visé et surtout du type de réseau ciblé (ethernet 10/100Mbps/1Gbps, Wifi, 3G, etc.) mais en général, il est conseillé d’éviter que la taille des ressources téléchargées soit inférieure à 5Ko et supérieure à 500Ko. Ces bornes doivent permettre de cadrer le degré de modularité à adopter. Pour cela, l’utilisation de l’outil SOYC (Story Of Your Compile, inclus dans GWT 2.0) permettra de répartir équitablement le poids de l’application entre les différents modules durant les phases de développement. En effet, l’outil génère des rapports mettant en relief le poids de chaque module, ce qui permet d’identifier les modules dont le poids ne rentre pas dans les bornes qu’on se fixe (ex : 500Ko > poids > 5Ko).

Conclusion

En respectant les bonnes pratiques web, en architecturant et en modularisant les applications GWT avec un mécanisme de chargement/téléchargement à la demande des modules, on répond à la problématique de temps de chargement. Au final, peu importe le nombre de vues implémentées dans l’application, celle-ci est capable d’en accueillir autant qu’on le souhaite sans impacter son temps de démarrage. Si on reprend le graphique des temps de téléchargement et d’initialisation, on voit qu’on tend vers un lissage du temps de chargement entre la vue initiale et les vues suivantes :

graphique_modularisation_lazy

Cet article présente des concepts illustrés par des exemples d'utilisation de la technologie GWT. Ces concepts (et notamment le téléchargement à la demande) existent également en Flex.