AngularJS
Profilowanie i wydajność
Szukaj…
7 prostych ulepszeń wydajności
1) Używaj oszczędnie powtarzania ng
Używanie ng-repeat
w widokach ogólnie powoduje słabą wydajność, szczególnie gdy są zagnieżdżone ng-repeat
.
To jest bardzo wolne!
<div ng-repeat="user in userCollection">
<div ng-repeat="details in user">
{{details}}
</div>
</div>
Staraj się unikać jak najwięcej zagnieżdżonych powtórzeń. Jednym ze sposobów na poprawę wydajności ng-repeat
jest użycie funkcji track by $index
(lub innego pola identyfikatora). Domyślnie ng-repeat
śledzi cały obiekt. Z track by
, kątowe zegarki obiekt jedynie $index
lub ID obiektu.
<div ng-repeat="user in userCollection track by $index">
{{user.data}}
</div>
Używaj innych metod, takich jak paginacja , przewijanie wirtualne , nieskończone przewijanie lub limitTo: zacznij, gdy to możliwe, aby uniknąć iteracji po dużych kolekcjach.
2) Powiąż raz
Angular ma dwukierunkowe wiązanie danych. Koszt jest zbyt wolny, jeśli jest używany zbyt często.
Wolniejsza wydajność
<!-- Default data binding has a performance cost -->
<div>{{ my.data }}</div>
Szybsza wydajność (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>
Użycie notacji „powiązaj raz” mówi Angularowi, aby poczekał, aż wartość ustabilizuje się po pierwszej serii cykli trawienia. Angular użyje tej wartości w DOM, a następnie usunie wszystkich obserwatorów, aby stała się wartością statyczną i nie była już związana z modelem.
{{}}
Jest znacznie wolniejszy.
To ng-bind
jest dyrektywą i spowoduje umieszczenie obserwatora na przekazywanej zmiennej. Więc ng-bind
będzie obowiązywał tylko wtedy, gdy przekazana wartość faktycznie się zmieni.
Z drugiej strony nawiasy zostaną sprawdzone i odświeżone przy każdym $digest
, nawet jeśli nie będzie to konieczne.
3) Funkcje zakresu i filtry wymagają czasu
AngularJS ma pętlę podsumowania. Wszystkie funkcje są widoczne, a filtry są uruchamiane za każdym razem, gdy uruchamia się cykl podsumowania. Pętla podsumowania będzie wykonywana za każdym razem, gdy model zostanie zaktualizowany i może spowolnić twoją aplikację (filtr może zostać wciśnięty wiele razy przed załadowaniem strony).
Unikaj tego:
<div ng-controller="bigCalulations as calc">
<p>{{calc.calculateMe()}}</p>
<p>{{calc.data | heavyFilter}}</p>
</div>
Lepsze podejście
<div ng-controller="bigCalulations as calc">
<p>{{calc.preCalculatedValue}}</p>
<p>{{calc.data | lightFilter}}</p>
</div>
Gdzie kontroler może być:
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 oglądających
Obserwatorzy ogromnie obniżają wydajność. Przy większej liczbie obserwatorów pętla podsumowania potrwa dłużej, a interfejs użytkownika zwolni. Jeśli obserwator wykryje zmianę, uruchomi pętlę podsumowania i ponownie wyrenderuje widok.
Istnieją trzy sposoby ręcznego obserwowania zmiennych zmian w Angular.
$watch()
- obserwuje zmiany wartości
$watchCollection()
- obserwuje zmiany w kolekcji (ogląda więcej niż zwykły $watch
)
$watch(..., true)
- Unikaj tego tak bardzo, jak to możliwe, wykona „głębokie oglądanie” i obniży wydajność (ogląda więcej niż watchCollection
)
Zwróć uwagę, że jeśli wiążesz zmienne w widoku, tworzysz nowe zegarki - użyj {{::variable}}
aby zapobiec tworzeniu zegarka, szczególnie w pętlach.
W rezultacie musisz śledzić, ilu obserwatorów używasz. Za pomocą tego skryptu możesz policzyć obserwatorów (kredyt dla @Words jak Jared Liczba obserwatorów )
(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
Funkcje te są bardzo podobne w zachowaniu. ng-if
usuwa elementy z DOM, podczas gdy ng-show
ukrywa tylko elementy, ale zachowuje wszystkie programy obsługi. Jeśli masz części kodu, których nie chcesz wyświetlać, użyj ng-if
.
Zależy to od rodzaju użytkowania, ale często jedno jest bardziej odpowiednie niż drugie.
Jeśli element nie jest potrzebny, użyj
ng-if
Aby szybko włączyć / wyłączyć, użyj
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>
W razie wątpliwości - użyj ng-if
i przetestuj!
6) Wyłącz debugowanie
Domyślnie dyrektywy i zakresy bind pozostawiają dodatkowe klasy i znaczniki w kodzie, aby pomóc w różnych narzędziach do debugowania. Wyłączenie tej opcji oznacza, że nie renderujesz już tych różnych elementów podczas cyklu podsumowania.
angular.module('exampleApp', []).config(['$compileProvider', function ($compileProvider) {
$compileProvider.debugInfoEnabled(false);
}]);
7) Użyj zastrzyku zależności, aby ujawnić swoje zasoby
Dependency Injection to wzorzec projektowania oprogramowania, w którym obiekt otrzymuje swoje zależności, a nie sam obiekt, który je tworzy. Chodzi o usunięcie zakodowanych zależności i umożliwienie ich zmiany w razie potrzeby.
Można się zastanawiać nad kosztem wydajności związanym z analizowaniem ciągów wszystkich funkcji wstrzykiwanych. Angular dba o to, buforując właściwość $ inject po raz pierwszy. Tak się nie dzieje za każdym razem, gdy trzeba wywołać funkcję.
WSKAZÓWKA PRO: Jeśli szukasz podejścia zapewniającego najlepszą wydajność, skorzystaj z metody adnotacji $ inject. Podejście to całkowicie unika analizowania definicji funkcji, ponieważ logika ta jest zawinięta w następujący sposób sprawdzenia w funkcji adnotacji: if (! ($ Inject = fn. $ Inject)). Jeśli $ inject jest już dostępny, nie jest wymagane parsowanie!
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 WSKAZÓWKA 2: Możesz dodać dyrektywę ng-strict-di
do tego samego elementu, co ng-app
aby włączyć tryb ścisłej DI, który spowoduje zgłoszenie błędu za każdym razem, gdy usługa próbuje użyć ukrytych adnotacji. Przykład:
<html ng-app="DemoApp" ng-strict-di>
Lub jeśli używasz ręcznego ładowania:
angular.bootstrap(document, ['DemoApp'], {
strictDi: true
});
Wiąż raz
Angular ma reputację wspaniałego dwukierunkowego wiązania danych. Domyślnie Angular nieprzerwanie synchronizuje wartości powiązane między modelem a komponentami widoku za każdym razem, gdy zmieniają się dane w modelu lub komponencie widoku.
Koszt ten jest nieco powolny, jeśli jest używany zbyt często. Będzie to miało większy wpływ na wydajność:
Zła wydajność: {{my.data}}
Dodaj dwa dwukropki ::
przed nazwą zmiennej, aby użyć wiązania jednorazowego. W takim przypadku wartość jest aktualizowana dopiero po zdefiniowaniu my.data. Wyraźnie wskazujesz, aby nie obserwować zmian danych. Angular nie wykona żadnych kontroli wartości, w wyniku czego w każdym cyklu podsumowania będzie ocenianych mniej wyrażeń.
Przykłady dobrych wyników z użyciem jednorazowego wiązania
{{::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>
Uwaga: powoduje to jednak usunięcie dwukierunkowego powiązania danych dla my.data
, więc za każdym razem, gdy to pole zmienia się w twojej aplikacji, to samo nie zostanie odzwierciedlone w widoku automatycznie. Dlatego używaj go tylko do wartości, które nie zmienią się przez cały okres użytkowania aplikacji .
Funkcje zakresu i filtry
AngularJS ma pętlę podsumowania, a wszystkie funkcje w widoku i filtry są wykonywane przy każdym uruchomieniu cyklu podsumowania. Pętla podsumowania będzie wykonywana za każdym razem, gdy model zostanie zaktualizowany i może spowolnić twoją aplikację (filtr może zostać wciśnięty wiele razy, zanim strona zostanie załadowana).
Powinieneś tego unikać:
<div ng-controller="bigCalulations as calc">
<p>{{calc.calculateMe()}}</p>
<p>{{calc.data | heavyFilter}}</p>
</div>
Lepsze podejście
<div ng-controller="bigCalulations as calc">
<p>{{calc.preCalculatedValue}}</p>
<p>{{calc.data | lightFilter}}</p>
</div>
Gdzie próbka kontrolera to:
.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
});
Obserwatorzy
Obserwatorzy potrzebni do obejrzenia pewnej wartości i wykrycia, że ta wartość została zmieniona.
Po wywołaniu $watch()
lub $watchCollection
nowy obserwator dodaj do wewnętrznej kolekcji obserwatora w bieżącym zakresie.
Czym więc jest obserwator?
Watcher to prosta funkcja, która jest wywoływana w każdym cyklu podsumowania i zwraca pewną wartość. Angular sprawdza zwracaną wartość, jeśli nie jest taka sama jak w poprzednim wywołaniu - wywołanie zwrotne przekazane w drugim parametrze do funkcji $watch()
lub $watchCollection
zostanie wykonane.
(function() {
angular.module("app", []).controller("ctrl", function($scope) {
$scope.value = 10;
$scope.$watch(
function() { return $scope.value; },
function() { console.log("value changed"); }
);
}
})();
Obserwatorzy są zabójcami wydajności. Im więcej masz obserwatorów, tym dłużej trwa tworzenie pętli podsumowującej, tym wolniejszy jest interfejs użytkownika. Jeśli obserwator wykryje zmiany, uruchomi pętlę podsumowania (ponowne obliczenie na wszystkich ekranach)
Istnieją trzy sposoby ręcznego obserwowania zmiennych zmian w Angular.
$watch()
- tylko obserwuje zmiany wartości
$watchCollection()
- obserwuje zmiany w kolekcji (ogląda więcej niż zwykły zegarek $)
$watch(..., true)
- Unikaj tego tak bardzo, jak to możliwe, wykona „głębokie oglądanie” i zabije występ (ogląda więcej niż watchCollection)
Pamiętaj, że jeśli wiążesz zmienne w widoku, tworzysz nowych obserwatorów - użyj {{::variable}}
aby nie tworzyć obserwatora, szczególnie w pętlach
W rezultacie musisz śledzić, ilu obserwatorów używasz. Za pomocą tego skryptu możesz policzyć obserwatorów (kredyt dla @Words Like Jared - Jak policzyć całkowitą liczbę zegarków na stronie?
(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);
})();
Jeśli nie chcesz tworzyć własnego skryptu, istnieje narzędzie open source o nazwie ng-stats, które korzysta z wykresu w czasie rzeczywistym osadzonego na stronie, aby uzyskać wgląd w liczbę zegarków zarządzanych przez Angulara, a także częstotliwość i czas trwania cykli trawienia w czasie. Narzędzie udostępnia globalną funkcję o nazwie showAngularStats
, którą można wywołać, aby skonfigurować sposób działania wykresu.
showAngularStats({
"position": "topleft",
"digestTimeThreshold": 16,
"autoload": true,
"logDigest": true,
"logWatches": true
});
Powyższy przykładowy kod automatycznie wyświetla następujący wykres na stronie ( interaktywne demo ).
ng-if vs ng-show
Funkcje te są bardzo podobne w zachowaniu. Różnica polega na tym, że ng-if
usuwa elementy z DOM. Jeśli są duże części kodu, które nie zostaną pokazane, to ng-if
jest właściwą drogą. ng-show
ukryje tylko elementy, ale zachowa wszystkie programy obsługi.
ng-if
Dyrektywa ngIf usuwa lub odtwarza część drzewa DOM na podstawie wyrażenia. Jeśli wyrażenie przypisane do ngIf ma wartość fałszywą, element jest usuwany z DOM, w przeciwnym razie klon elementu jest ponownie wstawiany do DOM.
ng-show
Dyrektywa ngShow pokazuje lub ukrywa dany element HTML na podstawie wyrażenia podanego w atrybucie ngShow. Element jest pokazywany lub ukryty przez usunięcie lub dodanie klasy ng-hide CSS do elementu.
Przykład
<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>
Wniosek
Zależy to od rodzaju użycia, ale często jedno jest bardziej odpowiednie niż drugie (np. Jeśli 95% czasu element nie jest potrzebny, użyj ng-if
; jeśli chcesz ng-show
widoczność elementu DOM, użyj ng-show
).
W razie wątpliwości użyj ng-if
i przetestuj!
Uwaga : ng-if
tworzy nowy izolowany zakres, podczas gdy ng-show
i ng-hide
nie. Użyj $parent.property
jeśli właściwość zakresu nadrzędnego nie jest w niej bezpośrednio dostępna.
Przedstaw swój model
<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>
W powyższym przykładzie ustawiamy wartość odbicia na 1000 milisekund, która wynosi 1 sekundę. Jest to znaczne opóźnienie, ale zapobiegnie wielokrotnemu przebijaniu sygnału wejściowego przez ng-model
wieloma cyklami $digest
.
Używając debounce w polach wejściowych i wszędzie tam, gdzie natychmiastowa aktualizacja nie jest wymagana, możesz znacznie zwiększyć wydajność swoich aplikacji Angular. Nie tylko możesz opóźnić czas, ale możesz także opóźnić uruchomienie akcji. Jeśli nie chcesz aktualizować modelu ng przy każdym naciśnięciu klawisza, możesz także aktualizować po rozmyciu.
Zawsze wyrejestrowuj słuchaczy zarejestrowanych w innych zakresach niż obecny zakres
Zawsze musisz wyrejestrować zakresy inne niż obecny zakres, jak pokazano poniżej:
//always deregister these
$rootScope.$on(...);
$scope.$parent.$on(...);
Nie musisz wyrejestrowywać list bieżących z obecnego zakresu, ponieważ zajmowałby się tym kąt:
//no need to deregister this
$scope.$on(...);
$rootScope.$on
słuchaczach pozostanie w pamięci, jeśli przejdziesz do innego kontrolera. Spowoduje to wyciek pamięci, jeśli kontroler wyłączy się z zakresu.
Nie rób
angular.module('app').controller('badExampleController', badExample);
badExample.$inject = ['$scope', '$rootScope'];
function badExample($scope, $rootScope) {
$rootScope.$on('post:created', function postCreated(event, data) {});
}
Robić
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();
});
}