Blog
Eviter le clignotement de vos pages (AngularJS)

Partager

Mal utilisé, AngularJS ne rend pas correctement la page : le rendering est saccadé et les blocs "bougent " le temps que toutes les données soient chargées et correctement bindées.

Nous allons voir les techniques existantes pour proposer un rendu plus propre.

1. Ng-bind

Pour assigner une variable à un template HTML, il est possible d’écrire :

<span>{{user.name}}</span>

Bien que valide, cette syntaxe présente deux inconvénients :

  • Le template est affiché dans sa forme brute le temps que l’application se lance. Autrement dit la variable entre {{}} n’est pas encore interprétée et apparaît brièvement à l’utilisateur.
  • L’exécution de la page est ralentie car la valeur entre {{}} est tout le temps vérifiée (dirty checked) et rafraichie dans chaque cycle $digest, même lorsque la variable ne change pas.

La solution à ces deux problèmes est d’utiliser systématiquement la directive ng-bind :

<span ng-bind="user.name"></span>

Depuis la version 1.3, il est même possible de "bind once" via la syntaxe "::". Cela signifie que si vous savez qu’une variable ne changera pas une fois qu’elle est définie, cette expression ne sera plus surveillée par AngularJS et la boucle $digest sera allégée.

Exemple sur le bloc affiché une fois connecté sur un site

<span ng-bind="::'Bienvenue ' + user.email" ></span>

Note : La directive ng-cloak permet d'attendre que l'application soit lancée et de ne pas afficher le template non compilé. Cette directive n'est pas nécessaire si vous utilisez les ng-bind. Un des cas où cette directive peut-être utile est lorsque des {{}} sont utilisés sur des attributs. Exemple :

<input data-ng-cloak placeholder="{{'Email' | t}}" ng-model="user.email" />

2. Resolve (ui-router)

Une approche basique pour charger des données consiste à effectuer une requête asynchrone en début de controller et assigner le résultat à une variable du scope.

L’inconvénient est que la requête asynchrone peut mettre un certain temps à s’effectuer et du coup, le template HTML est partiellement rendu (il manque les données dynamiques).

Le module ui-router propose un mécanisme pour ne pas charger une vue tant que les données asynchrones n’ont pas été reçues : le resolve.

Les paramètres de resolve peuvent être des promises qui seront résolus avant que le controller soit instancié.

//Fichier de configuration de vos routes
$stateProvider
.state('root.user', {
    url: '/user',
    templateUrl: 'user/account.html,
    resolve: {
        useProfile:  function($http){
            // $http returns a promise for the url data
            return $http({method: 'GET', url: '/user/profile'});
        }
    },
    controller: 'UserCtrl'
});
//Dans le controller, on injecte le service créé dans le resolve :
angular.module('app')
    .controller('UserCtrl', ['$scope', 'useProfile', function($scope, useProfile) {
         $scope.info = useProfile;
    }]);

3. Caching de template

Lorsque vous désirez charger un fichier HTML (via un ng-include ou templateUrl avec ui-router), une requête http est effectuée.

Quand bien même cette requête ne durerait que quelques millisecondes, le rendu serait saccadé.

AngularJS fournit un service permettant de mettre en cache des fichiers HTML : $templateCache.

On peut l’utiliser facilement de la façon suivante :

$templateCache.put('template.html', '<div><b>This is a nice template</b></div>');

Le premier paramètre est la clé, le second la valeur.

On peut ensuite récupérer le template de la façon suivante :

$http.get('template.html', {cache:$templateCache});

N.B: Si la clé n’existe pas dans le cache, une requête sera lancée

Cette technique n’est cependant pas applicable sur de gros projets car les templates sont stockés dans des fichiers HTML dédiés et pas dans des chaînes de caractères.

À la place, nous utilisons GruntJS pour créer une tâche qui ajoute tous nos templates dans le cache :

 'file-creator': {
  'template_cache': {
    files: [
    {
      file: 'app.tpl.js',
      method: function(fs, fd, done) {
        var glob = grunt.file.glob;
        var Q = require('q');
        var deferred = Q.defer();
        deferred.resolve();
        var fileData = {};
        deferred.promise.then(function() {
          var readHtmlDeferred= Q.defer();
          glob('app/**/*.html', function (err, files) {
            for (var i = 0; i < files.length; i++) {
              var filename = files[i];
              fileData[filename] = grunt.file.read(files[i]);
            }
            readHtmlDeferred.resolve();
          });
          return readHtmlDeferred.promise;
      }).then(function() {
        fs.writeSync(fd, wrap_loader(fileData));
        done();
      }, function() {
        // error
      });
      function  wrap_loader(fileMap) {
        return   '' +
            '(function(){' +
              'angular.module(\app.templates\').run([\'$templateCache\',function($templateCache){' +
                'var cache=' + JSON.stringify(fileMap) + ';' +
                'for(var i in cache){if(cache.hasOwnProperty(i))$templateCache.put(i,cache[i])};' +
              '}]);' +
            '})();';
        }
      }
    }
  ]
},

Nous pouvons ensuite récupérer normalement nos templates via la méthode $http.get décrite plus haut ou via le paramètre templateUrl de nos routes (ui-router pioche d'abord dans le $templateCache avant d'effectuer une requête http).

comments powered by Disqus