Optymalizowanie wykrywania zmian w Angular

Szybsze wykrywanie zmian z myślą o wygodzie użytkowników

Angular okresowo uruchamia mechanizm wykrywania zmian, dzięki czemu zmiany w modelu danych są odzwierciedlane w widoku aplikacji. Wykrywanie zmian może być aktywowane ręcznie lub przez zdarzenie asynchroniczne (np. interakcja użytkownika lub wykonanie XHR).

Wykrywanie zmian to zaawansowane narzędzie, ale jeśli jest uruchamiane bardzo często, może wywołać wiele obliczeń i zablokować główny wątek przeglądarki.

Z tego posta dowiesz się, jak kontrolować i optymalizować mechanizm wykrywania zmian poprzez pomijanie części aplikacji i uruchamianie wykrywania zmian tylko wtedy, gdy jest to konieczne.

Wykrywanie zmian w Inside Angular

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 dwóch działów firmy – działu sprzedaży oraz badań i rozwoju – i składa się z dwóch elementów:

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

Przykładowa aplikacja

W szablonie dla AppComponent możesz zobaczyć 2 wystąpienia tekstu 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 swoje imię i nazwisko oraz wartość liczbową. Aplikacja przekazuje wartość liczbową pracownika do obliczeń firmy i wizualizuje wynik na ekranie.

.

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 akceptuje jako dane wejściowe listę pracowników i nazwę działu. Gdy użytkownik próbuje usunąć lub dodać pracownika, komponent aktywuje odpowiednie dane wyjściowe. Komponent określa też metodę calculate, która implementuje obliczenia związane z firmą.

Oto szablon dla adresu 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 powtarza procedurę dla wszystkich pracowników na liście i w przypadku każdego z nich renderuje jej pozycję. Zawiera też dyrektywę ngModel służącą do dwukierunkowego wiązania danych między danymi wejściowymi a właściwością label zadeklarowaną w zasadzie EmployeeListComponent.

Na podstawie 2 wystąpień EmployeeListComponent aplikacja tworzy to drzewo komponentów:

Drzewo komponentów

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

Gdy użytkownik zacznie wpisywać imię i nazwisko nowego pracownika w polu do wprowadzania danych w polu EmployeeListComponent, Angular aktywuje wykrywanie zmian dla całego drzewa komponentów, począwszy od AppComponent. Oznacza to, że gdy użytkownik wpisuje tekst, Angular wielokrotnie przelicza wartości liczbowe powiązane z poszczególnymi pracownikami, aby upewnić się, że nie zmieniły się od czasu ostatniego sprawdzenia.

Aby sprawdzić, jak wolno to może działać, otwórz niezoptymalizowaną wersję projektu w StackBlitz i spróbuj wpisać imię i nazwisko pracownika.

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

  1. Naciśnij „Control + Shift + J” (lub „Command + Option + J” na Macu), aby otworzyć Narzędzia deweloperskie.
  2. Kliknij kartę Skuteczność.

Teraz kliknij Rejestruj (w lewym górnym rogu panelu Skuteczność) i zacznij pisać w jednym z pól tekstowych w aplikacji. Za kilka sekund kliknij ponownie Nagraj , aby zatrzymać nagrywanie. Gdy Narzędzia deweloperskie w Chrome przetworzą wszystkie zebrane dane profilowania, zobaczysz coś takiego:

Profilowanie wydajności

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

Pomijam poddrzewa komponentów

Gdy użytkownik wpisuje tekst dotyczący sprzedaży EmployeeListComponent, wiesz, że dane w dziale badań i rozwoju się nie zmieniają – nie ma więc powodu, aby uruchamiać wykrywanie zmian w tym komponencie. Aby mieć pewność, że instancja R&D nie aktywuje wykrywania zmian, ustaw wartość changeDetectionStrategy parametru 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 {...}

Teraz gdy użytkownik wpisze tekst, wykrywanie zmian zostanie aktywowane tylko w przypadku odpowiedniego działu:

Wykrywanie zmian w drzewie podrzędnym komponentu

Informacje na temat optymalizacji zastosowanej do oryginalnej aplikacji znajdziesz tutaj.

Więcej informacji o strategii wykrywania zmian w usłudze OnPush znajdziesz w oficjalnej dokumentacji Angular.

Aby zobaczyć efekty optymalizacji, wpisz nowego pracownika w aplikacji w StackBlitz.

Korzystanie z czystych rur

Mimo że strategia wykrywania zmian w elemencie EmployeeListComponent jest teraz ustawiona na OnPush, Angular nadal przelicza wartości liczbowe wszystkich pracowników w danym dziale, gdy użytkownik wpisze odpowiedni tekst.

Aby poprawić to zachowanie, możesz użyć czystych rur. Zarówno czyste, jak i zanieczyszczone potoki akceptują dane wejściowe i zwracane wyniki, których można użyć w szablonie. Różnica między nimi polega na tym, że czysta kreska pionowa przeliczy wynik tylko wtedy, gdy otrzyma inne dane wejściowe z poprzedniego wywołania.

Pamiętaj, że aplikacja oblicza wartość do wyświetlenia na podstawie wartości liczbowej pracownika, wywołując metodę calculate zdefiniowaną w zasadzie EmployeeListComponent. Jeśli przeniesiesz obliczenia na czystą pionową kreskę, Angular ponownie obliczy wyrażenie tego typu tylko wtedy, gdy zmienią się jego argumenty. Platforma określi, czy argumenty kreski pionowej uległy zmianie, wykonując sprawdzenie odwołania. Oznacza to, że Angular nie wykona żadnych obliczeń ponownie, chyba że zaktualizujesz wartość liczbową pracownika.

Aby przenieść obliczenia biznesowe do potoku o nazwie 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 potoku wywołuje funkcję fibonacci. Zwróć uwagę, że kreska jest czysta. Angular uzna wszystkie rury za czyste, chyba że określisz inaczej.

Na koniec zaktualizuj wyrażenie w szablonie dla EmployeeListComponent:

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

Znakomicie. Teraz gdy użytkownik wpisze tekst powiązany z dowolnym działem, aplikacja nie obliczy ponownie wartości liczbowych poszczególnych pracowników.

W aplikacji poniżej możesz zobaczyć, o ile płynniejsze pisanie jest.

Aby zobaczyć efekt ostatniej optymalizacji, skorzystaj z tego przykładu w StackBlitz.

Kod z optymalizacją logiczną pierwotnej aplikacji jest dostępny tutaj.

Podsumowanie

Gdy w aplikacji Angular występują opóźnienia w czasie działania:

  1. Profiluj aplikację za pomocą Narzędzi deweloperskich w Chrome, aby zobaczyć, skąd się biorą spowolnienia.
  2. Przedstaw strategię wykrywania zmian OnPush, aby wyciąć poddrzewa komponentu.
  3. Przenieś duże obliczenia do czystych potoków, aby umożliwić platformie buforowanie obliczonych wartości.