Angular digest loop i watchery

Problemy z wydajnością aplikacji angular 1.x

Framework angular znany i lubiany jest za swój 2-ways data binding, czyli automatyczną synchronizację zmian pomiędzy danymi na widoku i w kontrolerze. Aby taka synchronizacja danych była możliwa wykorzystywane jest $watch API, do obserwowania zmian powstałych na danym obiekcie/modelu przypisanym do scope’a.

www.akra.net

Mówiąc prościej, Angular obserwuje zmiany, sprawdza i porównuje czy wartość danej zmiennej/obiektu się nie zmieniła. Jeśli się zmieniła synchronizuje jej wartość pomiędzy widokiem a kontrolerem, i tak w kółko (digest loop). Zazwyczaj, nowoczesne przeglądarki są dość szybkie i taka pętla do porównywania danych nie stanowi dla nich problemu. Jednak w miarę powiększania się naszej aplikacji, na pewno zauważymy spadek wydajności.

Takie pojedyncze obserwowanie zmian nazywamy watcherem. Duża liczba watcherów jest bardzo często odpowiedzialna za znaczący spadek wydajności naszej aplikacji.

Tworzone są one w dwóch przypadkach:

  • {{expression}} — wyrażenia w html
  • $scope.$watch(‘expression/function’) —w JavaScript

Narzędzia do kontrolowania ilości watcherów.

Do obserwowania ilości watcherów możemy wykorzystać jedno z dostępnych rozszerzeń do przeglądarki Chrome “Angular watchers” lub bardziej rozbudowane “Angular-Performance”.

Rozszerzenie do przeglądarki Chrome Angular watchers, jak widać na stronie mamy zarejestrowane 1,371 watcherów.

One-time binding na ratunek

Angular w wersji 1.3 wprowadza usprawnienie one-time binding, które w praktycznie bezbolesny sposób pozwala na znaczne zredukowanie liczby watcherów powstałych na warstwie widoku.

Składnia stosowana przy one-time binding.

Jak widać, w przykładzie kodu powyżej, aby zastosować one-time binding wystarczy przed wyrażenie zawarte w podwójnych nawiasach klamrowych dodać podwójny dwukropek. Od tego czasu angular zacznie sprawdzać czy zmienna ‘name’ jest różna od wartości null lub undefinend. Kiedy do zmiennej zostanie przypisana jakaś wartość, w naszym przypadku string, Angular przestaje obserwować zmienną ‘name’, tym sposobem liczba naszych watcherów zmiejszy się o jeden.

One-time binding w połączeniu z dyrektywą ngRepeat

O ile stosowanie wyrażeń typu {{expresion}} , zazwyczaj nie powoduje problemów z wydajnością, o tyle stosowanie tego typu wyrażeń w połączeniu z dyrektywą ngRepeat (powielającą dane wyrażenie np 10 000 razy), może stanowić spore obciążenie dla przeglądarki, tym samym obniżając znacząco czas renderowania aplikacji, w przypadku ngRepeat zapewne listy jakiś elementów.

Zazwyczaj dyrektywa ngRepeat służy nam do renderowania jakieś kolekcji danych. Rozważmy przykład renderowania listy użytkowników. Z serwera pobierany jest json, który następnie przekazywany jest do widoku. Na warstwie widoku, za pomocą dyrektywy ngRepeat renderujemy listę użytkowników. To wszystko czego oczekujemy od naszej aplikacji. Jednak Angular, dla każdego wyrażenia typu {{expresion}} tworzy watcher.

W przypadku listy użytkowników nie potrzebujemy obserwować zmian dla każdego rekordu. Oczywistym rozwiązaniem wydaje się zastosowanie one-time binding. Jednak tu mogą pojawiać się pewne niejasności.

Przykład użycia one-time binding w połączeniu z dyrektywą ngRepeat

Jak widać wyrażenie podwójnego dwókropka “::” możemy zastosować też wewnątrz ngRepeat. Rozważmy trzy przypadki:

  • Przypadek 1: Oba wyrażenia nie będą obserwowane. Dodanie użytkownika, lub zmiana jego imienia nie będzie od odzwierciedlona na widoku.
  • Przypadek 2: Pierwsze wyrażenie nie będzie obserwowane drugie już tak. Dodanie użytkownika nie będzie odzwierciedlona, jednak zmiana imienia użytkownika będzie zauważona na widoku.
  • Przypadek 3: Pierwsze wyrażenie nie będzie obserwowane. Dodanie nowego użytkownika nie będzie zauważalne na widoku. Drugie wyrażenie będzie obserwowane, dodanie numeru telefonu do użytkownika lub zmiana jego numeru telefonu będzie zauważalna.

Przykłady tych trzech przypadków wyjaśniane były również w wątku w serwisie stackoverflow.

One-time binding i biblioteka angular translate

Wiele aplikacji rozwijanych w Akra Polska posiada opcję wyboru języka. W tym celu bardzo często wykorzystujemy biblioteka angular-translate.

Biblioteka ta realizuje internacjonalizację treści naszej strony. Co ciekawe do wyboru mamy różne sposoby realizacji tego zadania.

  • Przypadek 1, wykorzystanie filtra: sposób który zapewne wykorzystamy jako pierwszy, jego składnia jest ładna. Jest on jednak bardzo nie efektywny ponieważ, jak już wiemy, dla każdego wyrażenia typu {{}} angular tworzy dodatkowy watcher.
  • Przypadek 2, wykorzystanie one-time binding: naturalnym rozwiązaniem problemu watcherów wydaje się zastosowanie wyrażenia podwójnego ::. Jednak w takiej sytuacji napotkamy na problem, szczególnie w połączeniu z dyrektywą ngRepeat. W takiej sytuacji możliwy jest scenariusz w którym wyrażenie zostanie przefiltrowane zanim serwis dostarczy wymagane tłumaczenia, w takim przypadku do wyrażenia przypisany zostanie pusty string, a angular usunie watcher, tym samym uniemożliwiając późniejsze uzupełnienie translacji.
  • Przypadek 3, wykorzystanie dyrektywy: sposób ten wydaje się najbardziej prawidłowy, i rekomendowany przez autorów biblioteki. Usuwa on zbędne watchery a aplikacji, i działa prawidłowo z ngRepeat

Podsumowanie

Jak widać dbanie o wydajność aplikacji napisanej z wykorzystaniem frameworka Angular 1.x nie jest skomplikowane. Dobre zrozumienie tematu jest jednak potrzebne.

Dla zainteresowanych polecić mogę stronę ng-perf, gdzie znaleźć można wiele cenny wskazówek dotyczących wydajności aplikacji.


Jeżeli zainteresował Cię artykuł, zapraszam do kontaktu z nami: praca@akra.net | www.akra.net | www.facebook.com/akra.net

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.