Intermédiaire

AngularJS : optimisations et bonnes pratiques

Comme tout framework, Angularjs nous aide énormément au début d’un projet, pour rapidement devenir un problème.
Dans cet article, nous allons voir comment optimiser notre utilisation d’AngularJS ainsi que quelques bonnes pratiques de développement afin de pouvoir maîtriser son développement.

::binding once

L’un des principaux problèmes arrive sur des écrans relativement complexes. Les performances s’effondrent, ce qui est particulièrement visible sur les devices mobiles (aux capacités souvent plus modestes qu’un PC).
Cela vient du trop grand nombre de watchers tournant simultanément.

Qu’est-ce qu’un watcher ?

Un watcher est créé lorsque l’on déclare dans un template l’utilisation d’une expression pour assurer la fonctionnalité de two-way binding.

Par exemple, lorsque l’on déclare :

<body>
   <p>Hello {{value+2}}!</p>
</body>

 un watcher est créé, lié à la l’expression value+2 du modèle
Un tableau de 50 lignes, avec une dizaine de cellule par ligne, dépasse les 500 watchers.

Un écran standard d’une application peut en compter plus de 6000.

A chaque ordre de mise à jour déclenché par le framework, ces expressions sont réévaluées.

Et AngularJS ADORE rafraîchir le modèle. Il le fait dès qu’une valeur change et beaucoup d’événements peuvent aussi le déclencher.

Pour utile que soit le two-way binding, il n’est nécessaire que dans peu de cas.

Depuis la version 1.3, il est possible de spécifier au framework d’évaluer l’expression une fois quand elle est disponible puis de désenregistrer le watcher.

Il suffit de faire précéder l’expression par “::”

<body>
   <p>Hello {{::value+2}}!</p>
</body>

Pour garantir des performances optimales, il faut l’appliquer dans tous les cas où la valeur ne change pas plus d’une fois.

Batarang

Une façon de traquer les watchers est d’installer l’extension batarang, qui permet de lister les watchers, et de repérer ceux qui sont redondants.

yeah

ng-repeat

Il est possible de n’évaluer un tableau qu’une seule fois via ng-repeat, en déclarant l’attribut ng-repeat comme suit :

<ul>
   <li ng-repeat=”value in :: collection”>{{::value.title}}</li>
 </ul>

Dans ce cas, on peut imaginer que la collection est chargée de manière asynchrone.

Toutefois, dans le cas d’une recherche où la collection en est le résultat, il faut laisser actif le two-way binding sur celle-ci, tout en gardant le binding once sur les éléments de ce tableau, car chacune des lignes sera recréée lors de la mise à jour de la collection.

Coté javascript :

$http.get(‘/someUrl’).then(function(response){
	$scope.collection = response.data;
});

Coté html :

<ul>
   <li ng-repeat=”value in collection”>{{::value.title}}</li>
 </ul>

Filter, translate, …

Il est préférable d’évaluer la valeur filtrée dans le contrôleur; le binding once fonctionne mal avec le filtre.

Ex :

<div>{{'title' | translate}}</div>

JS :

$scope.titleTranslated = $translate.instant('title');

html :

<div>{{titleTranslated}}</div>

Expressions booléennes

Les expressions booléennes sont piégeuses, car elles retournent toujours une valeur.

Il faut les encapsuler dans une fonction pouvant retourner null lorsque la valeur n’est pas encore disponible. Ainsi l’expression restera enregistrée dans les watchers jusqu’à ce qu’il soit possible de la calculer une fois.

A éviter

JS :

$http.get('/someUrl').then(function(response){
	$scope.value = response.data.value != '';
});

html :

<div ng-if="value">soat</div>

A faire
JS :

http.get('/someUrl').then(function(response){
 $scope.value = response.data.value != '';
});

$scope.isSoatVisible = isSoatVisible;

function isSoatVisible(){
 //cas non passant
 if($scope.value){
 return null;
 }

 return $scope.value;
}

html :

<div ng-if="::isSoatVisible()">soat</div>

Code style

Ces recommandations sont tirées des liens suivants :

Angular Style Guide de John Papa

Opinionated AngularJS styleguide for teams de Todd Motto

Organisation d’un module

Pour l’organisation d’un module, il y a plusieurs écoles, l’important étant surtout de s’y astreindre et de garder en tête le but premier : une meilleure lisibilité et maintenabilité.

Personnellement, je préconise de laisser toute la “magie d’Angular” tout en bas d’un module. Ensuite, je préfère regrouper les déclarations par ensembles cohérents.

D’abord la déclaration des variables internes, puis des variables bindées, suivies des fonctions bindées.

Les fonctions en tant que telles viennent tout à la fin du module, après le code.

function someModule() {
    /*--------------------------------------*\
    internal variable
    \*--------------------------------------*/
    var vm = $scope; // explication plus bas

    bind();
    activate();

    /*--------------------------------------*\
    Bind variable
    \*--------------------------------------*/
    function bind() {
        vm.isOk = true;
        vm.value = {‘
            title’: ‘the title’
        };
        vm.otherValue = false;
        vm.another = ['one', 'two', 'three'];

        /*--------------------------------------*\
        Bind function
        \*--------------------------------------*/
        vm.bindedFunction = bindedFunction;
        vm.otherBindedFunction = otherBindedFunction;
    };
    /*--------------------------------------*\
    module execution
    \*--------------------------------------*/
    function activate() {
        // launch some stuff
        bindedFunction(1500);
    };

    function bindedFunction(value) {…}

    function otherBindedFunction(valueA, valueB) {…}
}

Isolation des modules

Afin de ne pas polluer le scope javascript, il est préférable d’isoler chacun de vos modules dans une Immediately Invoked Function Expression (IIFE)

Qu’est-ce qu’une IIFE ?

Une IIFE supprime les variables du scope global : ce qui se passe dans une IIFE reste dans l’IIFE. Cela évite le problème de collision de nom de variable/function.

De même, il est préférable de créer un fichier par module.

Le projet gagnera en lisibilité et en maintenabilité, car le développeur ne manipulera plus des blocs monolithiques de 2500 lignes.

A éviter :

Un seul fichier controller.js

angular
    .module('app')
    .factory('logger', logger);
// logger function is added as a global variable
function logger() {}
angular
    .module('app')
    .factory('storage', storage);
// storage function is added as a global variable
function storage() {...
}
})();

A faire :

fichier logger.js

(function () {
    'use strict';
    angular
        .module('app')
        .factory('logger', logger);
    // logger function is added as a global variable
    function logger() {}
})();

fichier storage.js

(function () {
    'use strict';
    angular
        .module('app')
        .factory('storage', storage);
    // storage function is added as a global variable
    function storage() {}
})();

ng-if à la place de ng-show

Dans les cas où un élément html n’a pas pour vocation d’être affiché et masqué successivement, il est préférable d’utiliser ng-if à la place de ng-show. Ng-if va retirer l’élément en question au lieu de le cacher, allégeant par la même occasion le code html ainsi que les éventuelles déclarations de variables (et les watchers qui vont avec).

Par contre, si l’élément peut être amené à être ré-affiché, il faut utiliser ng-show, comme dans le cas d’une pop-up d’erreur, par exemple.

Injection et minification

Lors de la minification, les variables sont renommées de façon a réduire la taille finale d’un script.

La déclaration d’un contrôleur :

function mainController($location, $http) {}

devient :

function a(b, c) {}

Le framework AngularJS n’est donc plus capable de récupérer les services $location et $http
Il faut donc utiliser une autre manière de déclarer l’injection, en rappelant les injections Angular dans un tableau de chaine de caractères, celles-ci n’étant pas minifiées.

Contrôleur non compatible à la minification

(function () {
    'use strict';
    angular.module('webApp')
        .controller(function ($location, $http) {...
        });
})();

Contrôleur compatible

(function () {
    'use strict';
    angular.module('webApp')
        .controller(['$location', '$http',
function ($location, $http) {...}]);
})();

Il est même préférable d’isoler le contrôleur dans une fonction pour plus de lisibilité

(function () {
    'use strict';
    angular.module('webApp')
        .controller(['$location', '$http', mainCtrl]);

    function mainCtrl($location, $http) {...
    }
})();

Pour aller plus loin, il est aussi possible d’isoler aussi l’injection.

(function () {
 'use strict';
 angular.module('webApp')
 .controller(mainCtrl);
 function mainCtrl($location, $http) {...}
mainCtrl.$inject = ['$location', '$http'];
})();

ng-strict-di

Cette directive est TRÈS importante dans le développement d’une application.

Elle permet de lever une erreur dès qu’un contrôleur n’a pas été écrit de façon à être minifié.

<body ng-app="webApp" ng-strict-di>

disable debug

Pour un boost significatif des performances en production, il faut désactiver le debug angular.

(function () {
    'use strict';
    angular.module('webApp')
        .config(appConfig);

    function appConfig($compileProvider) {
        $compileProvider.debugInfoEnabled(false);
    }
    appConfig.$inject = ['$compileProvider'];
})();

Plus d’informations sur la doc officielle d’AngularJS.

Pas de logique dans les controlleurs

Trop souvent nous voyons des contrôleurs gargantuesques mais à la base les contrôleurs ne sont pas designer pour contenir du code métier.
Les contrôleurs ne doivent servir qu’à “câbler” des services avec l’application.
Ceci permet une meilleure testabilité et une meilleure réutilisabilité de ces logiques.

Il faut donc s’astreindre à appliquer ce pattern et déporter la logique métier dans les services

A eviter:

function MainCtrl () {
  this.doSomething = function () {...};
}
angular
  .module('app')
  .controller('MainCtrl', MainCtrl);
	

A faire:

function MainCtrl (SomeService) {
  this.doSomething = SomeService.doSomething;
}
angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

$scope

$scope va disparaitre avec Angular 2.

Il est possible d’ajuster les applications existantes afin de préparer la montée de version.

Ainsi nous pouvons renommer le scope en utilisant controllAs dans l’injection du contrôleur du template :

<div ng-controller="MainController">
{{value}}
</div>

devient

<div ng-controller="MainController as vm">
{{vm.value}}
</div>

Coté contrôleur, il n’est plus nécessaire d’injecter $scope, this y fait directement référence.

(function () {
    'use strict';
    angular.module('webApp')
        .controller(mainCtrl);

    function mainCtrl($scope) {
        $scope.value = ‘ok’;
    }
    mainCtrl.$inject = ['$scope'];
})();

devient

(function () {
    'use strict';
    angular.module('webApp')
        .controller(mainCtrl);

    function mainCtrl() {
        this.value = ‘ok’;
    }
    mainCtrl.$inject = [];
})();

Toutefois, étant source de confusion, l’utilisation du mot-clé this est à éviter en javascript, en particulier lorsque l’on y fait référence dans une fonction appelée de manière asynchrone (résultat d’un appel http, dans un timeout …).

Il vaut mieux l’isoler dans une variable, que l’on renommera du même nom que celui choisi dans la directive controllerAs.

(function () {
    'use strict';
    angular.module('webApp')
        .controller(mainCtrl); 
    function mainCtrl() {
        var vm = this;
        vm.value = ‘ok’;
    }
    mainCtrl.$inject = [];
})();

Récapitulons :

html :

<div ng-controller="MainController as vm">
{{vm.value}}
</div>

JS :

(function () {
    'use strict';
    angular.module('webApp')
        .controller(mainCtrl); 
    function mainCtrl() {
        var vm = this;
        vm.value = ‘ok’;
    }
    mainCtrl.$inject = [];
})();

Nous avons désormais les bases pour développer sereinement une application en maitrisant les performances et sa maintenabilité.
Je vous invite à consulter les sites de John Papa et de Todd Motto pour approfondir le sujet.

© SOAT
Toute reproduction interdite sans autorisation de la société SOAT

Nombre de vue : 3032

COMMENTAIRES 2 commentaires

  1. cedric dit :

    Bonjour,

    En testant le code suivant de votre tutoriel :
    (function () {
    ‘use strict’;
    angular.module(‘webApp’)
    .controller(mainCtrl);
    function mainCtrl() {
    var vm = this;
    vm.value = ‘ok’;
    }
    mainCtrl.$inject = [];
    })();

    Je me retrouve avec l’erreur:
    Error: [$injector:nomod] Module ‘webApp’ is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.

    En m’aidant de la documentation officiel (http://errors.angularjs.org/1.5.3/$injector/nomod?p0=webApp), j’ai fais le code ci-dessous qui fonctionne:
    var webApp = angular.module(‘webApp’, []);
    webApp.controller([‘$location’, ‘$http’, mainCtrl]);

    function mainCtrl($location, $http) {
    }

    Pouvez-vous me donner un exemple utilisable de votre idée consistant à isoler un controller (ou une factory)?

    Cédric.

  2. Fred dit :

    Bonjour Cédric.

    En effet, en l’état le code ne peux pas fonctionner, car il manque la déclaration dans angular du module webApp.

    Je la considérais à tord comme étant implicite.
    Toutefois il faut le faire dans un bloc IIFE dédié et ne pas l’affecter à une variable, car sa portée est limité au scope de l’IIFE.
    fichier webApp.js
    (function () {
    ‘use strict’;
    var dependencies = [
    ‘dependecie1’,
    ‘dependecie2’,
    ‘dependecie3′, …’];

    angular.module(‘webApp’, depedencies);
    })();

    fichier mainCtrl.js
    (function () {
    ‘use strict’;
    angular.module(‘webApp’)
    .controller(mainCtrl);
    function mainCtrl() {
    var vm = this;
    vm.value = ‘ok’;
    }
    mainCtrl.$inject = [];
    })();

AJOUTER UN COMMENTAIRE