AngularJS
Profilazione e prestazioni
Ricerca…
7 miglioramenti delle prestazioni semplici
1) Usa ng-repeat con parsimonia
L'uso di ng-repeat
nelle viste comporta generalmente scarse prestazioni, in particolare quando ci sono ng-repeat
annidate.
Questo è super lento!
<div ng-repeat="user in userCollection">
<div ng-repeat="details in user">
{{details}}
</div>
</div>
Cerca di evitare il più possibile le ripetizioni annidate. Un modo per migliorare le prestazioni di ng-repeat
è utilizzare la track by $index
(o qualche altro campo id). Di default, ng-repeat
traccia l'intero oggetto. Con track by
, Angular controlla l'oggetto solo dall'indice $index
o dall'id oggetto.
<div ng-repeat="user in userCollection track by $index">
{{user.data}}
</div>
Utilizza altri approcci come impaginazione , scroll virtuali , scroll infiniti o limitTo: inizia quando possibile per evitare iterazioni su raccolte di grandi dimensioni.
2) legare una volta
Angolare ha l'associazione dati bidirezionale. Viene fornito con un costo di essere lento se usato troppo.
Prestazioni più lente
<!-- Default data binding has a performance cost -->
<div>{{ my.data }}</div>
Prestazioni più veloci (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'uso della notazione "bind once" indica ad Angular di attendere che il valore si stabilizzi dopo la prima serie di cicli digest. Angular utilizzerà quel valore nel DOM, quindi rimuoverà tutti gli osservatori in modo che diventi un valore statico e non sia più legato al modello.
Il {{}}
è molto più lento.
Questo ng-bind
è una direttiva e posizionerà un osservatore sulla variabile passata. Quindi il ng-bind
si applicherà solo quando il valore passato cambia effettivamente.
Le parentesi d'altra parte saranno sporche controllate e aggiornate in ogni $digest
, anche se non è necessario.
3) Le funzioni e i filtri dell'oscilloscopio richiedono tempo
AngularJS ha un ciclo digest. Tutte le tue funzioni sono in una vista e i filtri vengono eseguiti ogni volta che viene eseguito il ciclo di digest. Il ciclo digest verrà eseguito ogni volta che il modello viene aggiornato e può rallentare la tua app (il filtro può essere colpito più volte prima che la pagina venga caricata).
Evita questo:
<div ng-controller="bigCalulations as calc">
<p>{{calc.calculateMe()}}</p>
<p>{{calc.data | heavyFilter}}</p>
</div>
Approccio migliore
<div ng-controller="bigCalulations as calc">
<p>{{calc.preCalculatedValue}}</p>
<p>{{calc.data | lightFilter}}</p>
</div>
Dove può essere il controller:
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 spettatori
Gli spettatori abbandonano tremendamente le prestazioni. Con più osservatori, il ciclo digest richiederà più tempo e l'interfaccia utente rallenterà. Se l'osservatore rileva il cambiamento, avvia il ciclo digest e ricrea la vista.
Esistono tre modi per eseguire la sorveglianza manuale per i cambiamenti variabili in Angolare.
$watch()
- guarda per le variazioni di valore
$watchCollection()
- osserva i cambiamenti nella collezione (guarda più del normale $watch
)
$watch(..., true)
- Evita il più possibile, eseguirà "deep watch" e watchCollection
le prestazioni (guarda più di watchCollection
)
Nota che se stai legando le variabili nella vista stai creando nuovi orologi: usa {{::variable}}
per impedire la creazione di un orologio, specialmente nei loop.
Di conseguenza è necessario tenere traccia di quanti osservatori stai usando. Puoi contare gli osservatori con questo script (credito a @Words Like Jared Number of watchers )
(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
Queste funzioni sono molto simili nel comportamento. ng-if
rimuove gli elementi dal DOM mentre ng-show
nasconde solo gli elementi ma mantiene tutti i gestori. Se hai parti del codice che non vuoi mostrare, usa ng-if
.
Dipende dal tipo di utilizzo, ma spesso uno è più adatto dell'altro.
Se l'elemento non è necessario, utilizzare
ng-if
Per attivare / disattivare rapidamente, utilizzare
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>
In caso di dubbio, usa ng-if
e prova!
6) Disabilitare il debug
Per impostazione predefinita, le direttive e gli ambiti di bind lasciano classi e markup aggiuntivi nel codice per aiutare con vari strumenti di debug. Disabilitare questa opzione significa che non esegui più il rendering di questi vari elementi durante il ciclo di digest.
angular.module('exampleApp', []).config(['$compileProvider', function ($compileProvider) {
$compileProvider.debugInfoEnabled(false);
}]);
7) Usa l'iniezione delle dipendenze per esporre le tue risorse
Dependency Injection è un pattern di progettazione software in cui a un oggetto vengono assegnate le sue dipendenze, piuttosto che l'oggetto che le crea da sé. Si tratta di rimuovere le dipendenze hard-coded e rendere possibile cambiarle quando necessario.
Potreste chiedervi il costo delle prestazioni associato a tale analisi delle stringhe di tutte le funzioni iniettabili. Angular si occupa di ciò memorizzando nella cache la proprietà $ inject dopo la prima volta. Quindi questo non accade ogni volta che una funzione deve essere invocata.
SUGGERIMENTO PRO: se stai cercando l'approccio con le migliori prestazioni, vai con l'approccio annotazione delle proprietà $ inject. Questo approccio evita completamente l'analisi della definizione della funzione poiché questa logica è racchiusa all'interno del seguente controllo nella funzione annotate: if (! ($ Inject = fn. $ Inject)). Se $ inject è già disponibile, non è richiesta alcuna analisi!
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);
PRO TIP 2: è possibile aggiungere una direttiva ng-strict-di
sullo stesso elemento di ng-app
per attivare la modalità DI rigorosa che genera un errore ogni volta che un servizio tenta di utilizzare annotazioni implicite. Esempio:
<html ng-app="DemoApp" ng-strict-di>
O se usi il bootstrap manuale:
angular.bootstrap(document, ['DemoApp'], {
strictDi: true
});
Bind Once
Angular ha la reputazione di avere un eccezionale binding di dati bidirezionale. Per impostazione predefinita, Angular sincronizza continuamente i valori associati tra il modello e i componenti di visualizzazione in qualsiasi momento in cui i dati cambiano nel componente del modello o della vista.
Questo ha un costo di essere un po 'lento se usato troppo. Questo avrà un successo in termini di prestazioni più grandi:
Cattiva performance: {{my.data}}
Aggiungi due punti ::
prima del nome della variabile per utilizzare l'associazione una tantum. In questo caso, il valore viene aggiornato solo una volta definito my.data. Stai indicando esplicitamente di non guardare le modifiche dei dati. Angular non eseguirà alcun controllo del valore, risultante con un numero inferiore di espressioni valutate in ciascun ciclo di digestione.
Buoni esempi di prestazioni che utilizzano l'associazione una tantum
{{::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>
Nota: questo rimuove l'associazione bidirezionale dei dati per my.data
, quindi ogni volta che questo campo cambia nell'applicazione, lo stesso non si rifletterà automaticamente nella vista. Quindi usalo solo per valori che non cambieranno per tutta la durata della tua applicazione .
Funzioni e filtri dell'ambito
AngularJS ha un ciclo di digest e tutte le tue funzioni in una vista e i filtri vengono eseguiti ogni volta che viene eseguito il ciclo di digest. Il ciclo digest verrà eseguito ogni volta che il modello viene aggiornato e può rallentare la tua app (il filtro può essere colpito più volte, prima che la pagina venga caricata).
Dovresti evitare questo:
<div ng-controller="bigCalulations as calc">
<p>{{calc.calculateMe()}}</p>
<p>{{calc.data | heavyFilter}}</p>
</div>
Approccio migliore
<div ng-controller="bigCalulations as calc">
<p>{{calc.preCalculatedValue}}</p>
<p>{{calc.data | lightFilter}}</p>
</div>
Dove il campione del controller è:
.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
});
osservatori
Gli osservatori necessari per guardare un certo valore e rilevare che questo valore è cambiato.
Dopo la chiamata $watch()
o $watchCollection
nuovo watcher aggiungere alla raccolta interna del watcher nell'ambito corrente.
Quindi, che cos'è l'osservatore?
Watcher è una funzione semplice, che viene richiamata in ogni ciclo di digest e restituisce un valore. Angolare controlla il valore restituito, se non è uguale a come era nella chiamata precedente - una richiamata che è stata passata nel secondo parametro alla funzione $watch()
o $watchCollection
verrà eseguita.
(function() {
angular.module("app", []).controller("ctrl", function($scope) {
$scope.value = 10;
$scope.$watch(
function() { return $scope.value; },
function() { console.log("value changed"); }
);
}
})();
Gli osservatori sono assassini di prestazioni. Più osservatori hai, più tempo impiegano a fare un ciclo digest, l'interfaccia utente più lenta. Se un osservatore rileva delle modifiche, avvia il ciclo digest (ricalcolo su tutto lo schermo)
Esistono tre modi per eseguire la visualizzazione manuale delle modifiche variabili in Angolare.
$watch()
- guarda solo per cambiamenti di valore
$watchCollection()
- osserva i cambiamenti nella collezione (guarda più del normale $ watch)
$watch(..., true)
- Evita il più possibile, eseguirà "deep watch" e ucciderà la performance (guarda più di watchCollection)
Nota che se stai legando le variabili nella vista, stai creando nuovi osservatori - usa {{::variable}}
non creare watcher, specialmente nei loop
Di conseguenza è necessario tenere traccia di quanti osservatori stai usando. Puoi contare gli osservatori con questo script (credito a @Words Like Jared - Come contare il numero totale di orologi su una pagina?
(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);
})();
Se non vuoi creare il tuo script, c'è una utility open source chiamata ng-stats che usa un grafico in tempo reale incorporato nella pagina per darti un'idea del numero di orologi che Angular sta gestendo, così come frequenza e durata dei cicli digest nel tempo. L'utilità espone una funzione globale denominata showAngularStats
che è possibile chiamare per configurare il modo in cui si desidera far funzionare il grafico.
showAngularStats({
"position": "topleft",
"digestTimeThreshold": 16,
"autoload": true,
"logDigest": true,
"logWatches": true
});
Il codice di esempio sopra mostra automaticamente il seguente grafico sulla pagina ( demo interattiva ).
ng-if vs ng-show
Queste funzioni sono molto simili nel comportamento. La differenza è che ng-if
rimuove gli elementi dal DOM. Se ci sono grandi parti del codice che non verranno mostrate, allora ng-if
è la strada da percorrere. ng-show
nasconderà solo gli elementi ma manterrà tutti i gestori.
ng-se
La direttiva ngIf rimuove o ricrea una porzione dell'albero DOM in base a un'espressione. Se l'espressione assegnata a ngIf restituisce un valore falso, l'elemento viene rimosso dal DOM, altrimenti un clone dell'elemento viene reinserito nel DOM.
ng-spettacolo
La direttiva ngShow mostra o nasconde l'elemento HTML dato in base all'espressione fornita all'attributo ngShow. L'elemento è mostrato o nascosto rimuovendo o aggiungendo la classe CSS ng-hide sull'elemento.
Esempio
<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>
Conclusione
Dipende dal tipo di utilizzo, ma spesso uno è più adatto dell'altro (ad esempio, se il 95% delle volte l'elemento non è necessario, utilizzare ng-if
; se è necessario attivare o disattivare la visibilità dell'elemento DOM, utilizzare ng-show
).
In caso di dubbio, usa ng-if
e prova!
Nota : ng-if
crea un nuovo ambito isolato, mentre ng-show
e ng-hide
no. Usa $parent.property
se la proprietà dell'ambito genitore non è direttamente accessibile in essa.
Rimbalza il tuo modello
<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>
Nell'esempio sopra stiamo impostando un valore di antirimbalzo di 1000 millisecondi che è 1 secondo. Questo è un ritardo considerevole, ma impedirà l'input da ripetutamente ng-model
thrash con molti cicli $digest
.
Utilizzando il debounce sui campi di input e in qualsiasi altro punto in cui non è richiesto un aggiornamento istantaneo, è possibile aumentare notevolmente le prestazioni delle app Angular. Non solo puoi ritardare il tempo, ma puoi anche ritardare quando l'azione viene attivata. Se non si desidera aggiornare il modello ng su ogni sequenza di tasti, è anche possibile eseguire l'aggiornamento su sfocatura.
Cancellare sempre gli ascoltatori registrati su altri ambiti diversi dall'ambito corrente
È sempre necessario annullare la registrazione degli ambiti diversi dall'ambito corrente, come illustrato di seguito:
//always deregister these
$rootScope.$on(...);
$scope.$parent.$on(...);
Non è necessario annullare la registrazione dei listener in base allo scope corrente, poiché angular si prenderà cura di esso:
//no need to deregister this
$scope.$on(...);
$rootScope.$on
ascoltatori rimarrà in memoria se $rootScope.$on
un altro controller. Ciò creerà una perdita di memoria se il controller non rientra nell'ambito.
non
angular.module('app').controller('badExampleController', badExample);
badExample.$inject = ['$scope', '$rootScope'];
function badExample($scope, $rootScope) {
$rootScope.$on('post:created', function postCreated(event, data) {});
}
Fare
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();
});
}