AngularJS
Profilage et performance
Recherche…
7 améliorations simples de la performance
1) Utilisez ng-repeat avec parcimonie
L'utilisation de ng-repeat
dans les vues entraîne généralement de mauvaises performances, en particulier lorsque des ng-repeat
sont imbriqués.
C'est super lent!
<div ng-repeat="user in userCollection">
<div ng-repeat="details in user">
{{details}}
</div>
</div>
Essayez d'éviter les répétitions imbriquées autant que possible. Une façon d'améliorer les performances de ng-repeat
est d'utiliser track by $index
(ou un autre champ id). Par défaut, ng-repeat
suit l'objet entier. Avec track by
, Angular ne regarde l'objet que par l' $index
ou object.
<div ng-repeat="user in userCollection track by $index">
{{user.data}}
</div>
Utilisez d'autres approches telles que la pagination , les parchemins virtuels , les parchemins infinis ou les limites .
2) Lier une fois
Angular a une liaison de données bidirectionnelle. Cela a un coût pour être lent s'il est trop utilisé.
Performance plus lente
<!-- Default data binding has a performance cost -->
<div>{{ my.data }}</div>
Performances plus rapides (AngularJS> = 1.3)
<!-- Bind once is much faster -->
<div>{{ ::my.data }}</div>
<div ng-bind="::my.data"></div>
<!-- Use single binding notation in ng-repeat where only list display is needed -->
<div ng-repeat="user in ::userCollection">
{{::user.data}}
</div>
L'utilisation de la notation "Lier une fois" indique à Angular d'attendre que la valeur se stabilise après la première série de cycles de digestion. Angular utilisera cette valeur dans le DOM, puis supprimera tous les observateurs de sorte qu'il devienne une valeur statique et ne soit plus lié au modèle.
Le {{}}
est beaucoup plus lent.
Ce ng-bind
est une directive et placera un observateur sur la variable transmise. Ainsi, ng-bind
ne s'appliquera que lorsque la valeur passée changera réellement.
Par contre, les crochets seront vérifiés et rafraîchis dans chaque $digest
, même si ce n’est pas nécessaire.
3) Les fonctions de portée et les filtres prennent du temps
AngularJS a une boucle de résumé. Toutes vos fonctions sont dans une vue et les filtres sont exécutés à chaque exécution du cycle de digestion. La boucle de résumé sera exécutée chaque fois que le modèle est mis à jour et cela peut ralentir votre application (le filtre peut être atteint plusieurs fois avant le chargement de la page).
Éviter ceci:
<div ng-controller="bigCalulations as calc">
<p>{{calc.calculateMe()}}</p>
<p>{{calc.data | heavyFilter}}</p>
</div>
Meilleure approche
<div ng-controller="bigCalulations as calc">
<p>{{calc.preCalculatedValue}}</p>
<p>{{calc.data | lightFilter}}</p>
</div>
Où le contrôleur peut être:
app.controller('bigCalulations', function(valueService) {
// bad, because this is called in every digest loop
this.calculateMe = function() {
var t = 0;
for(i = 0; i < 1000; i++) {
t += i;
}
return t;
}
// good, because this is executed just once and logic is separated in service to keep the controller light
this.preCalulatedValue = valueService.valueCalculation(); // returns 499500
});
4 spectateurs
Les observateurs abandonnent énormément la performance. Avec plus d'observateurs, la boucle de résumé prendra plus de temps et l'interface utilisateur ralentira. Si l'observateur détecte un changement, il lancera la boucle de résumé et restituera la vue.
Il existe trois façons de surveiller manuellement les changements de variables dans Angular.
$watch()
- surveille les changements de valeur
$watchCollection()
- surveille les changements dans la collection (regarde plus que $watch
habituel)
$watch(..., true)
- Évitez cela autant que possible, il effectuera une "montre profonde" et diminuera la performance (regarde plus que watchCollection
)
Notez que si vous liez des variables dans la vue, vous créez de nouvelles montres - utilisez {{::variable}}
pour éviter de créer une montre, en particulier dans les boucles.
En conséquence, vous devez suivre le nombre de visiteurs que vous utilisez. Vous pouvez compter les observateurs avec ce script (crédit à @Words Like Jared Nombre de spectateurs )
(function() {
var root = angular.element(document.getElementsByTagName('body')),
watchers = [],
f = function(element) {
angular.forEach(['$scope', '$isolateScope'], function(scopeProperty) {
if(element.data() && element.data().hasOwnProperty(scopeProperty)) {
angular.forEach(element.data()[scopeProperty].$$watchers, function(watcher) {
watchers.push(watcher);
});
}
});
angular.forEach(element.children(), function(childElement) {
f(angular.element(childElement));
});
};
f(root);
// Remove duplicate watchers
var watchersWithoutDuplicates = [];
angular.forEach(watchers, function(item) {
if(watchersWithoutDuplicates.indexOf(item) < 0) {
watchersWithoutDuplicates.push(item);
}
});
console.log(watchersWithoutDuplicates.length);
})();
5) ng-if / ng-show
Ces fonctions ont un comportement très similaire. ng-if
supprime les éléments du DOM alors que ng-show
cache uniquement les éléments mais garde tous les gestionnaires. Si vous ne souhaitez pas afficher certaines parties du code, utilisez ng-if
.
Cela dépend du type d'utilisation, mais souvent l'un convient mieux que l'autre.
Si l'élément n'est pas nécessaire, utilisez
ng-if
Pour activer / désactiver rapidement, utilisez
ng-show/ng-hide
<div ng-repeat="user in userCollection"> <p ng-if="user.hasTreeLegs">I am special<!-- some complicated DOM --></p> <p ng-show="user.hasSubscribed">I am awesome<!-- switch this setting on and off --></p> </div>
En cas de doute - utilisez ng-if
et testez!
6) Désactiver le débogage
Par défaut, les directives de liaison et les étendues laissent des classes et des balises supplémentaires dans le code pour aider à divers outils de débogage. La désactivation de cette option signifie que vous ne restituez plus ces différents éléments pendant le cycle de digestion.
angular.module('exampleApp', []).config(['$compileProvider', function ($compileProvider) {
$compileProvider.debugInfoEnabled(false);
}]);
7) Utiliser l'injection de dépendance pour exposer vos ressources
Dependency Injection est un modèle de conception logicielle dans lequel un objet reçoit ses dépendances, plutôt que l'objet qui les crée lui-même. Il s'agit de supprimer les dépendances codées en dur et de les modifier chaque fois que nécessaire.
Vous pourriez vous interroger sur le coût des performances associé à une telle analyse syntaxique de toutes les fonctions injectables. Angular s'en charge en mettant en cache la propriété $ inject après la première fois. Cela ne se produit donc pas chaque fois qu'une fonction doit être invoquée.
CONSEIL PRO: Si vous recherchez une approche offrant les meilleures performances, utilisez l'approche d'annotation de la propriété $ inject. Cette approche évite entièrement l'analyse de définition de fonction car cette logique est incluse dans la vérification suivante dans la fonction d'annotation: if (! ($ Inject = fn. $ Inject)). Si $ inject est déjà disponible, aucune analyse n'est requise!
var app = angular.module('DemoApp', []);
var DemoController = function (s, h) {
h.get('https://api.github.com/users/angular/repos').success(function (repos) {
s.repos = repos;
});
}
// $inject property annotation
DemoController['$inject'] = ['$scope', '$http'];
app.controller('DemoController', DemoController);
CONSEIL PRO 2: Vous pouvez ajouter une directive ng-strict-di
sur le même élément que ng-app
pour opter en mode DI strict, ce qui provoquera une erreur à chaque fois qu'un service essaiera d'utiliser des annotations implicites. Exemple:
<html ng-app="DemoApp" ng-strict-di>
Ou si vous utilisez un amorçage manuel:
angular.bootstrap(document, ['DemoApp'], {
strictDi: true
});
Lier une fois
Angular a la réputation d'avoir une liaison de données bidirectionnelle impressionnante. Par défaut, Angular synchronise en permanence les valeurs liées aux composants du modèle et de la vue à tout moment les modifications de données dans le composant de modèle ou de vue.
Cela a un coût pour être un peu lent s'il est trop utilisé. Cela aura un impact plus important sur la performance:
Mauvaises performances: {{my.data}}
Ajoutez deux colons ::
avant le nom de la variable pour utiliser la liaison unique. Dans ce cas, la valeur est mise à jour uniquement lorsque my.data est défini. Vous pointez explicitement pour ne pas surveiller les modifications de données. Angular n'effectue aucune vérification de valeur, ce qui réduit le nombre d'expressions évaluées à chaque cycle de digestion.
Bons exemples de performances utilisant une liaison unique
{{::my.data}}
<span ng-bind="::my.data"></span>
<span ng-if="::my.data"></span>
<span ng-repeat="item in ::my.data">{{item}}</span>
<span ng-class="::{ 'my-class': my.data }"></div>
Remarque: Ceci supprime toutefois la liaison de données bidirectionnelle pour my.data
, de sorte que chaque fois que ce champ change dans votre application, la même chose ne sera pas automatiquement reflétée dans la vue. Donc, utilisez-le uniquement pour les valeurs qui ne changeront pas pendant toute la durée de vie de votre application .
Fonctions et filtres de portée
AngularJS a la boucle de résumé et toutes vos fonctions dans une vue et les filtres sont exécutés chaque fois que le cycle de digestion est exécuté. La boucle de résumé sera exécutée chaque fois que le modèle est mis à jour et cela peut ralentir votre application (le filtre peut être atteint plusieurs fois avant le chargement de la page).
Vous devriez éviter ceci:
<div ng-controller="bigCalulations as calc">
<p>{{calc.calculateMe()}}</p>
<p>{{calc.data | heavyFilter}}</p>
</div>
Meilleure approche
<div ng-controller="bigCalulations as calc">
<p>{{calc.preCalculatedValue}}</p>
<p>{{calc.data | lightFilter}}</p>
</div>
Où échantillon de contrôleur est:
.controller("bigCalulations", function(valueService) {
// bad, because this is called in every digest loop
this.calculateMe = function() {
var t = 0;
for(i = 0; i < 1000; i++) {
t = t + i;
}
return t;
}
//good, because it is executed just once and logic is separated in service to keep the controller light
this.preCalulatedValue = valueService.caluclateSumm(); // returns 499500
});
Observateurs
Les observateurs nécessaires pour surveiller certaines valeurs et détecter que cette valeur est modifiée.
Après l'appel de $watch()
ou $watchCollection
nouvel observateur ajoute à la collection d'observateurs internes dans la portée actuelle.
Alors, qu'est-ce que l'observateur?
Watcher est une fonction simple, appelée à chaque cycle de digestion et qui renvoie une valeur. Angular vérifie la valeur renvoyée, si ce n'est pas la même chose que lors de l'appel précédent - un rappel transmis en second paramètre à la fonction $watch()
ou $watchCollection
sera exécuté.
(function() {
angular.module("app", []).controller("ctrl", function($scope) {
$scope.value = 10;
$scope.$watch(
function() { return $scope.value; },
function() { console.log("value changed"); }
);
}
})();
Les observateurs sont des tueurs de performance. Plus vous avez de observateurs, plus ils prennent de temps pour faire une boucle de lecture, l'interface utilisateur la plus lente. Si un observateur détecte des changements, il lancera la boucle de résumé (recalcul sur tous les écrans)
Il existe trois façons de faire une surveillance manuelle pour les changements de variables dans Angular.
$watch()
- surveille simplement les changements de valeur
$watchCollection()
- surveille les changements dans la collection (regarde plus que $ watch habituel)
$watch(..., true)
- Évitez cela autant que possible, il effectuera "deep watch" et tuera la performance (regarde plus que watchCollection)
Notez que si vous liez des variables dans la vue, vous créez de nouveaux observateurs - utilisez {{::variable}}
pour ne pas créer d’observateur, en particulier dans les boucles
En conséquence, vous devez suivre le nombre de visiteurs que vous utilisez. Vous pouvez compter les observateurs avec ce script (crédit à @Words Like Jared - Comment compter le nombre total de montres sur une page?
(function() {
var root = angular.element(document.getElementsByTagName("body")),
watchers = [];
var f = function(element) {
angular.forEach(["$scope", "$isolateScope"], function(scopeProperty) {
if(element.data() && element.data().hasOwnProperty(scopeProperty)) {
angular.forEach(element.data()[scopeProperty].$$watchers, function(watcher) {
watchers.push(watcher);
});
}
});
angular.forEach(element.children(), function(childElement) {
f(angular.element(childElement));
});
};
f(root);
// Remove duplicate watchers
var watchersWithoutDuplicates = [];
angular.forEach(watchers, function(item) {
if(watchersWithoutDuplicates.indexOf(item) < 0) {
watchersWithoutDuplicates.push(item);
}
});
console.log(watchersWithoutDuplicates.length);
})();
Si vous ne voulez pas créer votre propre script, il existe un utilitaire open source appelé ng-stats qui utilise un graphique en temps réel incorporé dans la page pour vous donner un aperçu du nombre de montres gérées par Angular, ainsi fréquence et durée des cycles de digestion au fil du temps. L'utilitaire expose une fonction globale nommée showAngularStats
que vous pouvez appeler pour configurer le fonctionnement du graphique.
showAngularStats({
"position": "topleft",
"digestTimeThreshold": 16,
"autoload": true,
"logDigest": true,
"logWatches": true
});
L'exemple de code ci-dessus affiche automatiquement le graphique suivant sur la page ( démonstration interactive ).
ng-if vs ng-show
Ces fonctions ont un comportement très similaire. La différence est que ng-if
supprime les éléments du DOM. S'il y a de grandes parties du code qui ne seront pas affichées, alors ng-if
est la voie à suivre. ng-show
ne fera que cacher les éléments mais gardera tous les gestionnaires.
ng-if
La directive ngIf supprime ou recrée une partie de l'arborescence DOM basée sur une expression. Si l'expression affectée à ngIf est évaluée à une valeur fausse, l'élément est supprimé du DOM, sinon un clone de l'élément est réinséré dans le DOM.
ng-show
La directive ngShow affiche ou masque l'élément HTML donné en fonction de l'expression fournie à l'attribut ngShow. L'élément est affiché ou masqué en supprimant ou en ajoutant la classe CSS ng-hide à l'élément.
Exemple
<div ng-repeat="user in userCollection">
<p ng-if="user.hasTreeLegs">I am special
<!-- some complicated DOM -->
</p>
<p ng-show="user.hasSubscribed">I am aweosme
<!-- switch this setting on and off -->
</p>
</div>
Conclusion
Cela dépend du type d'utilisation, mais souvent l'un est plus adapté que l'autre (par exemple, si 95% du temps, l'élément n'est pas nécessaire, utilisez ng-if
; si vous devez basculer la visibilité de l'élément DOM, utilisez ng-show
).
En cas de doute, utilisez ng-if
et testez!
Remarque : ng-if
crée une nouvelle étendue isolée, alors que ng-show
et ng-hide
ne le font pas. Utilisez $parent.property
si la propriété de la portée parent n'y est pas directement accessible.
Debounce votre modèle
<div ng-controller="ExampleController">
<form name="userForm">
Name:
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ debounce: 1000 }" />
<button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br />
</form>
<pre>user.name = </pre>
</div>
L'exemple ci-dessus définit une valeur de rebond de 1 000 millisecondes, soit 1 seconde. Il s'agit d'un retard considérable, mais empêchera l'entrée de supprimer à plusieurs reprises le ng-model
avec plusieurs cycles $digest
.
En utilisant l'anti-rebond sur vos champs de saisie et partout où une mise à jour instantanée n'est pas requise, vous pouvez augmenter considérablement les performances de vos applications angulaires. Non seulement vous pouvez retarder le temps, mais vous pouvez également retarder le déclenchement de l’action. Si vous ne souhaitez pas mettre à jour votre modèle ng à chaque frappe, vous pouvez également mettre à jour le flou.
Toujours désinscrire les auditeurs inscrits sur d'autres portées que l'étendue actuelle
Vous devez toujours désenregistrer les portées autres que votre portée actuelle, comme indiqué ci-dessous:
//always deregister these
$rootScope.$on(...);
$scope.$parent.$on(...);
Vous n'êtes pas obligé d'annuler l'enregistrement des auditeurs sur la portée actuelle car anguleux s'en chargerait:
//no need to deregister this
$scope.$on(...);
$rootScope.$on
écouteurs restera en mémoire si vous naviguez vers un autre contrôleur. Cela créera une fuite de mémoire si le contrôleur est hors de portée.
Ne pas
angular.module('app').controller('badExampleController', badExample);
badExample.$inject = ['$scope', '$rootScope'];
function badExample($scope, $rootScope) {
$rootScope.$on('post:created', function postCreated(event, data) {});
}
Faire
angular.module('app').controller('goodExampleController', goodExample);
goodExample.$inject = ['$scope', '$rootScope'];
function goodExample($scope, $rootScope) {
var deregister = $rootScope.$on('post:created', function postCreated(event, data) {});
$scope.$on('$destroy', function destroyScope() {
deregister();
});
}