Angular posiada mechanizm wykrywania zmian (Change Detector) dołączany do każdego komponentu, który odpowiada za synchronizację danych pomiędzy komponentem a jego templatką. To właśnie dzięki niemu drzewo DOM całej naszej aplikacji odzwierciedla aktualny stan aplikacji.

Mamy do dyspozycji dwie strategie detekcji zmian (ChangeDetectionStrategy):

  • Default
  • OnPush

Trzecią, najbardziej radykalną możliwością jest całkowite wyłączenie automatycznego sprawdzania zmian poprzez deaktywację biblioteki ngZone odpowiedzialnej za powiadamianie detektora o zaistniałym evencie. Ten wariant wykracza jednak poza ramy tego wpisu.

W jakich sytuacjach ngZone uruchamia Change Detector:

  • jakiekolwiek zdarzenie pochodzące z przeglądarki (click, keyup, onmouseover, itd.)
  • setInterval() i setTimeout()
  • requesty HTTP za pomocą XMLHttpRequest
  • Promise.then()
  • WebSocket.onmessage()
  • zmiana @Input
    • Default – zmiana wartości
    • OnPush – tylko zmiana referencji

Musimy zdawać sobie sprawę, że jeżeli jakieś asynchroniczne API nie jest wspierane przez Zone.js (jak np. IndexedDB callbacks), to o uruchamianie mechanizmu Change Detection będziemy musieli zadbać sami.

W aplikacji produkcyjnej po wystąpieniu zdarzenia Change Detector jest uruchamiany raz. Natomiast w trybie dewelopmentu jest on uruchamiany dwukrotnie po to, aby wykryć potencjalne problemy związane z tym, że wartość zmieniła się już po tym jak Angular przerenderował drzewo DOM.

Gdy już Change Detector zostanie uruchomiony w konkretnym komponencie, sprawdza każde template expression (czyli wartości zawarte w {{ expression }} lub jako binding [property]="expression") i sprawdza, czy jego wartość się zmieniła. Sposób porównania wartości jaki tutaj zachodzi to strict equality ===.

Domyślna strategia odświeżania danych

W domyślnej konfiguracji gdy uruchomiony zostanie Change Detector, wszystkie komponenty w drzewie sprawdzane są w kolejności od góry do samego dołu. W przypadku większych aplikacji z dużą ilością komponentów lub też znaczną ilością danych strategia ta może mieć negatywny wspływ na wydajność aplikacji.

Strategia OnPush odświeżania danych

Konfiguracja OnPush pozwala nam zoptymalizować ilość uruchamianych mechanizmów Change Detector (nie są uruchamiane dla wszystkich komponentów w każdym przypadku). Komponenty i ich dzieci nie są aktualizowane zawsze, a jedynie gdy:

  • referencja inputu się zmieniła (zmiana wartości w tym wypadku nie wystarczy)
  • to z tego komponentu lub jego dzieci zostało zainicjowane zdarzenie
  • change detection zostało wywołane ręcznie
  • observable wykorzystany w templatce za pomocą async pipe wyemitował nową wartość

Strategia OnPush najlepiej sprawdza się wraz z podejściem niemutowalności danych.

Debugging

Dzięki opcji dostępnej w narzędziach dla developerów w przeglądarce Google Chrome (Rendering – Paint flashing) jesteśmy w czasie rzeczywistym zobaczyć, które elementy strony są przez silnik przeglądarki przerenderowywane, a następnie dokonać optymalizacji tam, gdzie da ona największe korzyści.

Rendering – Paint flashing

Metody Change Detectora

ChangeDetectorRef.markForCheck() – oznacza wszystkie komponenty od roota do źródłowego jako dirty, Change Detector będzie na nich uruchomiony przy następnym cyklu, nawet jeżeli są to komponenty OnPush.

ChangeDetectorRef.detach() and ChangeDetectorRef.reattach() – możemy wyłączyć Change Detection dla danego komponentu i wywoływać go jedynie za pomocą detectChanges() w określonych przez nas przypadkach.

ChangeDetectorRef.detectChanges() – uruchamia Change Detection dla danego komponentu i jego dzieci.

ChangeDetectorRef.checkNoChanges() – wyrzuca błąd w wypadku, gdy zostały znalezione zmiany przez Change Detector w trakcie aktualnego cyklu.

ngZone.runOutsideAngular() – uruchomienie kodu poza ngZone, jeżeli nie chcemy aby w tym wypadku był wywoływany Change Detector.

Dodatkowa konfiguracja ngZone

Co ciekawe, istnieje również możliwość bardziej szczegółowego ustawienia ngZone poprzez utworzenie pliku konfiguracyjnego zone-flags.ts. Za jego pomocą możemy określić eventy, które nie chcemy aby wywoływały Change Detection. Szczegółowy opis wspomnianych ustawień znajdziecie w dokumentacji ngZone.

Optymalizacja kodu aplikacji

Gdy już mamy świadomość jak działa Change Detection w Angular, warto spojrzeć na to, czy pisany przez nas kod zachowuje się optymalnie w kontekście tego mechanizmu.

Funkcje w widokach

Działanie mechanizmu Change Detection sprawia, że za każdym razem musi on wywołać funkcję znajdującą się w widoku aby poznać jej wartość wynikową i porównać z dotychczasową. Bardzo często jest to wywołanie niepotrzebne, nadmiarowe. Za każdym razem, gdy dodajemy funkcję do widoku warto zastanowić się, czy nie będzie ona nadmiarowo wywoływana. Sposoby możliwej optymalizacji:

  • ngOnChanges lifecycle hook lub setter – zapisać wynik kalkulacji do osobnej propercji, a jej przekalkulowanie wywoływać tylko w określonych przypadkach,
  • pure pipe – każdy pipe jest domyślnie pure, tzn. zwrócona przez niego wartość jest przekalkulowywana jedynie w momencie, gdy zmianie ulegnie któryś z jego inputów.

Funkcja trackBy

trackBy pozwala opcjonalnie przekazać do do dyrektywy ngForOf funkcję, która jednoznacznie określi unikalny identyfikator każdego elementu listy i pozwoli przebudowywać tylko ten fragment drzewa DOM listy, który rzeczywiście uległ zmianie.