Single Page Apps : un risque de boulimie (AngularJS)

Un chargement initial qui devient coûteux.

AngularJS

Le concept de Single Page Application, dont la mise en oeuvre est l’un des usages les plus classiques d’AngularJS, présente rapidement un défaut majeur. En effet, dès lors qu’on développe une application un minimum conséquente, la quantité de Javascript et éventuellement de CSS nécessaire au premier chargement devient un frein majeur. On se retrouve donc à pénaliser fortement la première expérience de l’internaute.

Pourtant, ce dernier n’utilisera probablement que très partiellement l’application. Nous avons fait ce constat rapidement sur www.dareboost.com, avec une segmentation assez claire, puisqu’une partie importante de notre trafic utilisait le service de façon anonyme, sans accès à l’espace connecté de l’application. Pourquoi alors faire télécharger des données inutilisées par ce public ?

Injection de scripts à la volée, oui mais…

En théorie la réponse à ce problème de poids vient assez facilement : ne charger dans un premier temps que les parties utiles dans l’immédiat ou qui le seront très probablement. On chargera le reste en fonction des besoins lors de la navigation.

Un problème se pose lorsqu’on se penche sur cette idée: par défaut, AngularJS a besoin de l’ensemble du code Javascript (modules, controllers, services, directives, etc) lors de l’initialisation de l’application.
Si vous chargez un fichier Javascript en insérant une balise <script> “à chaud” comme dans cet exemple :

var anchor = document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0];
	var insertStyle = function (path) {
	var el = document.createElement('script');
	el.src = path;
	el.async = 1;
	anchor.insertBefore(el, anchor.lastChild);
}

Votre navigateur chargera et interprètera bien votre code. Mais AngularJS n’intègrera pas ce script, puisque pour faire prendre en compte de nouvelles ressources au framework, il est nécessaire de passer par son méanisme de providers (qui permettent l’enregistrement de modules, controllers ou autre.

La bonne nouvelle c’est que le module ocLazyLoad peut faire tout ça pour vous !

La solution : gestion de dépendances par module et LazyLoading

Si votre application est déjà découpé en modules “indépendants” vous pouvez facilement associer un ou plusieurs modules à une ou plusieurs URL/pages.

Ainsi il suffit lors d’un changement de page de charger les modules nécéssaire au bon fonctionnement de celle-ci (NB : le code javascript lié à un template doit être reconnu par angular avant l’affichage du template). On retrouve bien le concept général de LazyLoading : on ne charge que le nécessaire.

Le module oclazyload (https://github.com/ocombe/ocLazyLoad) est d’une grande aide dans le mise en place d’un tel mécanisme.

Il vous permet, entre autre, de configurer via la définition des routes le chargement des différentes parties de votre application.

Illustration avec quelques exemples de code

Prenons par exemple une application webmail, nous la découpons ici en trois modules

  • La boite de réception (lecture des mails)
  • La création de mail (l’envoi)
  • Le carnet d’adresse

Chacun des trois modules possède son fichier Javascript et éventuellement son fichier css contenant les styles propres aux pages du module.

Nous aurons également besoin d’un module principal qui sera chargé systématiquement et qui va gérer les fonctionnalitées globales de l’application (gestion des droits ou de l’affichage des messages d’erreur, etc).

Nom du module Fichier JavaScript Fichier CSS
inbox inbox.min.js inbox.min.css
create create.min.js create.min.css
contacts contacts.min.js contacts.min.css

Il nous reste à configurer les modules dans ocLazyLoad :

$ocLazyLoadProvider.config({
// module principal de l’application
loadedModules: ['app'],
// définition des différents modules qui seront chargés en lazy loading et association à leurs dépendances respectives
    modules: [
        {name: 'inbox', files: [inbox.min.js', ‘inbox.min.css']},
        {name: 'create', files: ['create.min.js', ‘create.min.css']},
{name: 'contacts', files: ['contacts.min.js', ‘contacts.min.css']},
    ],
});

Chaque module est donc bien associé à ses dépendances. Pour demander à ocLazyLoad de charger un module, on peut simplement appeler sa fonction : load(moduleName)

Pour s’assurer que le module soit chargé avant le template (afin notamment de faire fonctionner correctement les directives), on va inclure l’appel à la fonction “load” dans la définition de nos routes.

Définition des routes (version sans lazy loading):

$routeProvider.when('/inbox', {
     templateUrl : '/fragment/inbox’,
      controller: 'InboxCtrl',
})
.when('/write', {
     templateUrl : '/fragment/create’,
      controller: 'CreateCtrl',
})
.when('/contacts', {
     templateUrl : '/fragment/contacts’,
      controller: 'ContactsCtrl',
});

Ajout du lazyLoad :

$routeProvider.when('/inbox', {
     templateUrl : '/fragment/inbox’,
      controller: 'InboxCtrl',
resolve: { 
// Toutes les propriétés de “resolve” doivent retourner une promise
// et sont exécutuées avant que la vue ne soit chargée
        loadModule: ['$ocLazyLoad', function($ocLazyLoad) {
            // you can lazy load files for an existing module
            return $ocLazyLoad.load('inbox');
        }]
    }
})
.when('/create', {
     templateUrl : '/fragment/create’,
      controller: 'CreateCtrl',
resolve: { 
loadModule: ['$ocLazyLoad', function($ocLazyLoad) {
            return $ocLazyLoad.load('create');
        }]
    }
})
.when('/contacts', {
     templateUrl : '/fragment/contacts’,
      controller: 'ContactCtrl',
resolve: { 
loadModule: ['$ocLazyLoad', function($ocLazyLoad) {
        return $ocLazyLoad.load('contacts');
    }]
    }
});

Le code Javascript lié à chaque module étant chargé uniquement au changement de route (ou éventuellement autre appel explicite de ocLazyLoad.load) il ne faut pas oublier de changer vos habitudes lors de la déclaration du module principal de votre application : vos modules chargées en lazyload ne doivent plus en être des dépendances (puisqu’à ce stade, il ne sont pas connus) !

Sans utilisation du lazy loading :
angular.module('dareboost', [ 'ngRoute', 'create', 'inbox', 'contacts']);

Avec utilisation du lazy loading
angular.module('dareboost', [ 'oc.lazyLoad', 'ngRoute']);

Quelques points de vigilance :

Sur notre outil d’analyse de site web, nous avons rencontré 2 problèmes principaux lorsque nous avons commencé à utiliser ocLazyLoad :

  • les dépendances Javascript/CSS communes à plusieurs modules étaient potentiellement chargés plusieurs fois. Ce problème a été corrigé nativement dans le module depuis.
  • le CSS “actif” dépend du chemin de visite de l’internaute. Contrairement à un site web classique, ou on va tout recharger à chaque page, les Single Page Apps qui découpent de façon modulaires le CSS vont connaitre un phénomène d’accumulation du CSS actif au cours de la visite de l’internaute : attention aux conflits donc !

Aller plus loin :

Pour rester dans l’esprit du LazyLoading, nous avons mis en place sur DareBoost un mécanisme qui temporise la production du DOM généré par les directives, à la demande : au scroll de l’utilisateur. Peut-être le sujet d’un prochain article !

 

Partager l’article :

3 réflexions au sujet de « Single Page Apps : un risque de boulimie (AngularJS) »

  1. Salut à tous,

    Merci pour cet article fort intéressant. Je suis actuellement en train de travailler sur le sujet et je me trouve confronter à un soucis dont je n’ai pas encore trouvé la solution.

    Je souhaite charger des modules tiers avec ocLazyLoad, mais je ne sais pas comment les configurer dans le .config de mon module.

    Auriez vous une idée sur le sujet ?

    Bonne journée

  2. Bonjour,

    Pour charger des modules tiers qui nécessitent une configuration, vous devez les charger par le biais d’un de vos modules.

    Ce qui peut être fait de la manière suivante :
    En imaginant que votre module tiers ait pour nom : « moduleTiers », vous pouvez l’inclure dans les dépendances d’un de vos modules.

    angular.module(‘monModule’,[‘moduleTiers’]).config(…..

    Dans ce cas vous devez inclure les fichiers du module tiers dans la liste des fichiers liés à votre module (‘monModule’) dans la configuration ocLazyLoad (dans la définition des modules) :
    $ocLazyLoadProvider.config({debug: false,events: false,loadedModules: [‘dareboost’],
    modules: [
    {name: ‘monModule’, files: [‘monModule.min.js’, ‘monModule.min.css’, ‘moduleTiers.min.js’, ‘moduleTiers.min.css’]},
    … // vos autres modules
    ]
    });

    Si vous n’avez pas d’autres modules à charger en même temps que le module tiers, vous pouvez créer un module specifique qui se chargera seulement de configurer le module tiers.

    Il ne vous reste plus qu’à appeler la fonction : $ocLazyLoad.load(‘monModule’);

    En espérant vous avoir aidé.

    1. Merci beaucoup pour votre réponse, qui paraît évidente à première vue.

      Je devais avoir le nez dans le guidon pour ne pas le voir ;-)

      Et oui, votre réponse m’aide beaucoup.

      Vous souhaitant une agréable journée.

Les commentaires sont fermés.