Publié par
Il y a 2 années · 6 minutes · Front

Lazy loading avec WebPack & AngularJS

Angular and Webpack

Les outils de packaging web adoptent généralement deux approches opposées. Soit comme gulp et autre grunt, ils construisent un seul gros fichier concaténé et minifié, ce qui résulte en un temps de chargement initial important, notamment sur les supports mobiles. Soit une requête par fichier, comme require.js, ce qui entraîne le surcoût des requêtes et la latence lors du chargement de chaque page.

Seul Webpack fait les choses correctement !

Dans cet article je montre un cas pratique d’utilisation du lazy loading des ressources AngularJS avec Webpack.

Actuellement, je travaille pour une startup qui développe une solution de gestion de voitures connectées. Le début du projet remonte à la deuxième moitié de 2014. La stack front-office est classique pour cette « époque » : AngularJS 1.x en ES5 construit par Gulp, les dépendances gérées par Bower, feuilles de styles écrites en SASS.

Au fil des sprints, la base de code de l’application AngularJS a beaucoup grossi et l’importance des dépendances a augmenté de façon significative.

L’ensemble du code applicatif : Javascript, templates html ainsi que les bibliothèques externes forment un seul package « indivisible » . Même minifié, sa taille dépasse les 3 Mo. Cela peut sensiblement dégrader l’expérience utilisateur lors de l’ouverture d’application, surtout sur les terminaux mobiles et les réseaux 3G.

En fonction du rôle attribué à l’utilisateur, il accède à un sous-ensemble de l’application. La version mobile, implémentée en Responsive Web Design (RWD), donne accès à une page unique de l’application. Cependant, dans tous les cas, l’utilisateur télécharge l’ensemble de l’application dont une grande partie ne sera jamais exécuté.

Webpack à la rescousse

Si vous ne connaissez pas Webpack, je vous recommande vivement de lire l’excellent article Webpack, ES6 (ES2015) & Babel 6 pour modulariser son application Angular sur notre blog.

La plupart des articles qui expliquent comment combiner Webpack avec AngularJS laissent de côté un aspect important de Webpack : le Lazy Loading des dépendances, qui permet de charger uniquement les ressources nécessaires pour l’affichage d’une page ou de l’ensemble des pages liées fonctionnellement.

Webpack permet de configurer de multiples points d’entrées dans l’application. Ensuite, il construit une arbre de dépendances permettant de grouper et de récupérer uniquement les ressources nécessaires à chaque point d’entrée ainsi que les ressources communes aux différents points d’entrée. Chaque bloc de ce type est appelé bundle.

AngularJS

Cette approche n’est pas compatible avec AngularJS qui attend une déclaration de l’application à l’aide d’une directive ng-app. Heureusement, Webpack est très flexible et permet de déclarer les points de rupture en runtime dans le code :

class Greet {
 constructor() {
   this.name = 'I\'am Groot!';
 }
 tell() {
   console.log('Greet: ' + this.name);
 }
}
module.exports = Greet;
app.controller('appCtrl', () => {
 require(['./Greet.js'], (Greet) => {
   var greet = new Greet();
   greet.tell();
 });
});

La syntaxe require(['file.js'], function(file) {}) permet d’indiquer à Webpack que la liste de fichiers spécifiés ainsi que leurs dépendances doivent être placés dans un bundle suivant :

devtools-lazy-loading-syntaxe.png

Ici, 1.bundle.js contient uniquement la classe Greet, et bundle.js – le reste de l’application.

Problème…

J’ai rencontré un autre problème lorsque j’ai essayé de charger de la même façon un contrôleur ou un service d’AngularJS. Les dépendances chargées dynamiquement n’étaient pas visibles par le framework.

AngularJS n’a pas été conçu pour charger les dépendances dynamiquement. Une fois que l’application est initialisée – phase de configuration passée – la déclaration des nouveaux composants n’est plus possible de façon habituelle. C’est le cas pour tous les building blocks d’Angular: modules, services, contrôleurs et autres directives.

Heureusement, il y a un moyen de surmonter les limitations du framework. L’astuce est de rendre disponible en runtime les providers qui servent à déclarer les ressources AngularJS. Normalement, ces providers sont disponibles uniquement en phase de configuration.

Voici l’exemple du code permettant de faire l’ajustement :

app.config(($controllerProvider, $compileProvider, $filterProvider, $provide) => {
   app.register = {
     controller: $controllerProvider.register,
     directive: $compileProvider.directive,
     filter: $filterProvider.register,
     factory: $provide.factory,
     service: $provide.service
   };
 }
);

Par la suite :

app.register
 .service('aboutService', AboutService)

Notez que je passe par l’objet register créé précédemment pour déclarer un service.

Faire ce genre de manipulations manuellement est plutôt fastidieux. De plus, il est impossible de charger dynamiquement un module entier.

Heureusement, il existe une bibliothèque ad hoc qui a pour but de télécharger les modules AngularJS et de les déclarer dynamiquement: oclazyload. Étant donné que Webpack prend déjà en charge le chargement des ressources, nous utiliserons le oclazyload uniquement pour la déclaration dynamique des modules :

$ npm install oclazyload --save

Pour le routage nous utilisons, comme la plupart des projets Angular 1.x, une excellente bibliothèque : ui-router. Notez que les exemples suivants doivent également fonctionner avec le routeur de base.

L’idée principale est d’organiser chaque state (ou route) de l’application en modules angular qui seront chargés dynamiquement et indépendamment l’un de l’autre. Tout le découpage de code en « chunks » se fera au niveau de la configuration du routeur – très localisé et sans le boilerplate indésirable :

import angular from 'angular';
import uirouter from 'angular-ui-router';
import oclazyLoad from 'oclazyload';

angular.module('app', [oclazyLoad, uirouter])
 .config(function routing($stateProvider) {
   $stateProvider
     .state('hello', {
       url: '/hello',
       template: require('./hello/hello.html'),
       controller: 'helloCtrl as ctrl',
       resolve: {
         loadModule: ($q, $ocLazyLoad) => {
           return $q((resolve) => {
             require(['./hello/hello.module.js'], (module) => {
               resolve($ocLazyLoad.load({name: module.default}));
             });
           });
         }
       }
     })
     .state('about', {
       // ...
     });
 });

Le module app.hello n’a rien de particulier :

export default require('angular')
 .module('app.hello', [])
 .name;

require('./hello.ctrl');

Le code de ce module ainsi que ses dépendances éventuelles seront chargés dynamiquement et en un seul bundle uniquement quand l’utilisateur accédera à /hello.

Lors de la connexion initiale à /about :

devtools-lazy-loading-about.png

  • bundle.js – contient l’application angular avec les dépendances communes (angular.js, ui-router, etc.)
  • 2.bundle.js – module app.about chargé dynamiquement

Quand nous allons sur /hello :

devtools-lazy-loading-hello.png

  • 1.bundle.js – contient le module 'app.hello' avec ses dépendances.

Conclusion

La technique de lazy loading n’est pas indispensable pour toutes les applications. L’amélioration de l’expérience utilisateur sera surtout notable dans le cas des grosses applications qui comptent plusieurs dizaines de dépendances et qui sont accessibles via les terminaux mobiles. Cette solution présente aussi un intérêt lorsque les fonctionnalités accessibles varient beaucoup selon les rôles des utilisateurs.

Si vous pensez que votre application a besoin de la gestion de dépendances chargées à la demande, Webpack avec son bundling intelligent est un outil idéal pour votre application AngularJS.

Le code de l’application est disponible sur notre github.

Dmytro Podyachiy
Développeur full stack passionné par le monde open source, Dmytro travaille depuis plusieurs années dans l’écosystème Java sur des missions innovantes. Évangéliste du framework Angular, il l’utilise depuis presque 3 ans. Intéressé par les langages différents, il utilise notamment Scala et Javascript coté serveur.

Une réflexion au sujet de « Lazy loading avec WebPack & AngularJS »

  1. Publié par Maxime Gris, Il y a 2 années

    Bon article sur le lazy loading.
    Trois questions (liées) me viennent à l’esprit :
    Comment se passe la phase de création du projet pour production? Les dépendances définies en lazy loading peuvent être regroupées et minifiées? Ou doit-on subir un appel ajax pour chacune d’elles? (Pas tip top non plus)

  2. Publié par Disfigure, Il y a 1 année

    @Maxime Gris
    Les dependences en lazyloading je cite
    « Soit comme gulp et autre grunt, ils construisent un seul gros fichier concaténé et minifié, ce qui résulte en un temps de chargement initial important, notamment sur les supports mobiles »

    « L’ensemble du code applicatif : Javascript, templates html ainsi que les bibliothèques externes forment un seul package « indivisible » . Même minifié, sa taille dépasse les 3 Mo. Cela peut sensiblement dégrader l’expérience utilisateur lors de l’ouverture d’application, surtout sur les terminaux mobiles et les réseaux 3G. »

    « La technique de lazy loading n’est pas indispensable pour toutes les applications. L’amélioration de l’expérience utilisateur sera surtout notable dans le cas des grosses applications qui comptent plusieurs dizaines de dépendances et qui sont accessibles via les terminaux mobiles. Cette solution présente aussi un intérêt lorsque les fonctionnalités accessibles varient beaucoup selon les rôles des utilisateurs. »

    Resumé en gros faut lire de A à Z,

    by the way très bon article sur un point tres injustement oublié ou carrement méconnu des autres blogs owner.

    Regards.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *