Optymalizowanie wykrywania zmian w Angular

Wdrożenie szybszego wykrywania zmian, aby zapewnić lepsze wrażenia użytkownika.

Angular okresowo uruchamia mechanizm wykrywania zmian, aby zmiany w modelu danych były widoczne w widoku aplikacji. Wykrywanie zmian może być uruchamiane ręcznie lub za pomocą zdarzenia asynchronicznego (np. interakcji użytkownika lub zakończenia żądania XHR).

Wykrywanie zmian to przydatne narzędzie, ale jeśli jest wykonywane zbyt często, może wywoływać wiele obliczeń i blokować główny wątek przeglądarki.

Z tego artykułu dowiesz się, jak kontrolować i optymalizować mechanizm wykrywania zmian, pomijając części aplikacji i uruchamiając wykrywanie zmian tylko wtedy, gdy jest to konieczne.

Wykrywanie zmian w Angularze

Aby zrozumieć, jak działa wykrywanie zmian w Angular, przyjrzyjmy się przykładowej aplikacji.

Kod aplikacji znajdziesz w tym repozytorium GitHub.

Aplikacja zawiera listę pracowników z 2 działów firmy – sprzedaży i B+R – i ma 2 komponenty:

  • AppComponent, który jest głównym komponentem aplikacji,
  • 2 instancje EmployeeListComponent: jedna dla sprzedaży, a druga dla badań i rozwoju.

Przykładowa aplikacja

W szablonie AppComponent widać 2 wystąpienia zmiennej EmployeeListComponent:

<app-employee-list
  [data]="salesList"
  department="Sales"
  (add)="add(salesList, $event)"
  (remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [data]="rndList"
  department="R&D"
  (add)="add(rndList, $event)"
  (remove)="remove(rndList, $event)"
></app-employee-list>

Każdy pracownik ma nazwę i wartość liczbową. Aplikacja przekazuje wartość liczbową pracownika do obliczeń biznesowych i wizualizuje wynik na ekranie.

Teraz spójrz na EmployeeListComponent:

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
  @Input() data: EmployeeData[];
  @Input() department: string;
  @Output() remove = new EventEmitter<EmployeeData>();
  @Output() add = new EventEmitter<string>();

  label: string;

  handleKey(event: any) {
    if (event.keyCode === 13) {
      this.add.emit(this.label);
      this.label = '';
    }
  }

  calculate(num: number) {
    return fibonacci(num);
  }
}

EmployeeListComponent przyjmuje jako dane wejściowe listę pracowników i nazwę działu. Gdy użytkownik próbuje usunąć lub dodać pracownika, komponent uruchamia odpowiedni element wyjściowy. Komponent definiuje też metodę calculate, która implementuje obliczenia biznesowe.

Oto szablon dla EmployeeListComponent:

<h1 title="Department">{{ department }}</h1>
<mat-form-field>
  <input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
  <mat-list-item *ngFor="let item of data">
    <h3 matLine title="Name">
      {{ item.label }}
    </h3>
    <md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {{ calculate(item.num) }}
    </md-chip>
  </mat-list-item>
</mat-list>

Ten kod przetwarza wszystkich pracowników na liście i dla każdego z nich renderuje element listy. Zawiera ona też dyrektywę ngModel do dwukierunkowego wiązania danych między wejściem a właściwością label zadeklarowaną w deklaracji EmployeeListComponent.

Dzięki 2 wystąpieniom elementu EmployeeListComponent aplikacja tworzy tę strukturę komponentów:

Drzewo komponentów

AppComponent to element główny aplikacji. Jego komponenty podrzędne to 2 wystąpienia elementu EmployeeListComponent. Każda instancja ma listę elementów (E1, E2 itd.), które reprezentują poszczególnych pracowników w danym dziale.

Gdy użytkownik zacznie wpisywać nazwę nowego pracownika w polu wejściowym w EmployeeListComponent, Angular uruchamia wykrywanie zmian w całym drzewie komponentów, zaczynając od AppComponent. Oznacza to, że gdy użytkownik wpisze tekst, Angular wielokrotnie przelicza wartości liczbowe powiązane z każdym pracownikiem, aby sprawdzić, czy nie zmieniły się od ostatniego sprawdzenia.

Aby zobaczyć, jak wolno to może być, otwórz nieoptymalizowaną wersję projektu w StackBlitz i spróbuj wpisać imię i nazwisko pracownika.

Aby sprawdzić, czy spowolnienie wynika z funkcji fibonacci, utwórz projekt przykładowy i otwórz kartę Wydajność w Narzędziach deweloperskich w Chrome.

  1. Aby otworzyć Narzędzia dla programistów, naciśnij `Control+Shift+J` (lub `Command+Option+J` na Macu).
  2. Kliknij kartę Skuteczność.

Teraz kliknij Nagrywaj (w lewym górnym rogu panelu Wydajność) i zacznij pisać w jednym z pól tekstowych w aplikacji. Po kilku sekundach ponownie kliknij Nagrywaj , aby zatrzymać nagrywanie. Gdy narzędzia programistyczne Chrome przetworzą zebrane dane do profilowania, zobaczysz coś takiego:

Profilowanie wydajności

Jeśli na liście jest wielu pracowników, proces ten może zablokować wątek interfejsu przeglądarki i spowodować spadek liczby klatek, co może negatywnie wpłynąć na wygodę użytkowników.

Pomijanie poddrzew komponentów

Gdy użytkownik wpisze tekst w polu tekstowym w sekcji sprzedaży EmployeeListComponent, wiesz, że dane w dziale B+R się nie zmieniają, więc nie ma powodu, aby wykrywać zmiany w tym komponencie. Aby mieć pewność, że wystąpienie w ramach badań i rozwoju nie spowoduje wykrywania zmian, ustaw wartość parametru changeDetectionStrategy w komponencie EmployeeListComponent na OnPush:

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

Gdy użytkownik wpisze tekst, wykrywanie zmian zostanie uruchomione tylko w przypadku odpowiedniego działu:

Wykrywanie zmian w poddrzewie komponentu

Tę optymalizację zastosowano w pierwotnym zgłoszeniu tutaj.

Więcej informacji o strategii wykrywania zmian OnPush znajdziesz w oficjalnej dokumentacji Angulara.

Aby zobaczyć efekt tej optymalizacji, dodaj nowego pracownika w aplikacji StackBlitz.

Używanie czystych rur

Mimo że strategia wykrywania zmian dla elementu EmployeeListComponent jest teraz ustawiona na OnPush, Angular nadal przelicza wartość liczbową dla wszystkich pracowników w danym dziale, gdy użytkownik wpisze odpowiedni tekst.

Aby poprawić to zachowanie, możesz skorzystać z czystych przewodów. Zarówno czyste, jak i nieczyste przepływy danych akceptują dane wejściowe i zwracają wyniki, które można wykorzystać w szablonie. Różnica między tymi dwoma elementami polega na tym, że czysta funkcja przekierowania ponownie oblicza swój wynik tylko wtedy, gdy otrzyma dane wejściowe inne niż w poprzednim wywołaniu.

Pamiętaj, że aplikacja oblicza wartość do wyświetlenia na podstawie wartości numerycznej pracownika, wywołując metodę calculate zdefiniowaną w EmployeeListComponent. Jeśli przeniesiesz obliczenia do czystej pionowej kreski, Angular będzie ponownie obliczać wyrażenie tylko wtedy, gdy zmienią się jego argumenty. Framework sprawdzi, czy argumenty potoku uległy zmianie, wykonując sprawdzenie odwołania. Oznacza to, że Angular nie będzie wykonywać żadnych ponownych obliczeń, chyba że zostanie zaktualizowana wartość liczbowa pracownika.

Aby przenieść obliczenia biznesowe do elementu CalculatePipe:

import { Pipe, PipeTransform } from '@angular/core';

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform(val: number) {
    return fibonacci(val);
  }
}

Metoda transform w przesyłce wywołuje funkcję fibonacci. Zwróć uwagę, że rura jest czysta. Angular będzie uważać wszystkie znaki „|” za czyste, chyba że określisz inaczej.

Na koniec zaktualizuj wyrażenie w szablonie w miejscu EmployeeListComponent:

<mat-chip-list>
  <md-chip>
    {{ item.num | calculate }}
  </md-chip>
</mat-chip-list>

Znakomicie. Gdy użytkownik wpisze tekst powiązany z danym działem, aplikacja nie będzie już ponownie obliczać wartości liczbowej dla poszczególnych pracowników.

W aplikacji poniżej możesz zobaczyć, jak płynniej można teraz pisać.

Aby zobaczyć efekt ostatniej optymalizacji, wypróbuj ten przykład w StackBlitz.

Kod z optymalizacją czystego potoku w pierwotnej aplikacji jest dostępny tutaj.

Podsumowanie

Jeśli w aplikacji Angular występują spowolnienia w czasie działania:

  1. Wykonaj profilowanie aplikacji za pomocą Narzędzi deweloperskich w Chrome, aby sprawdzić, co powoduje spowolnienie.
  2. Wprowadź strategię wykrywania zmian OnPush, aby przyciąć poddrzewa komponentu.
  3. Przesuń wymagające dużej mocy obliczeniowej obliczenia do czystych przewodów, aby umożliwić frameworkowi buforowanie obliczonych wartości.