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ć 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 nazwę i wartość liczbową. Aplikacja przekazuje wartość liczbową pracownika do obliczeń biznesowych 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 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 powtarza procedurę dla wszystkich pracowników na liście i w przypadku każdego z nich renderuje jej pozycję. Zawiera ona też dyrektywę ngModel do dwukierunkowego wiązania danych między wejściem a właściwością label zadeklarowaną w deklaracji EmployeeListComponent.

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

Drzewo komponentów

AppComponent jest głównym komponentem 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 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, skonfiguruj 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 Nagraj (w lewym górnym rogu panelu Wydajność) i zacznij pisać w jednym z pól tekstowych w aplikacji. Po kilku sekundach kliknij ponownie Nagraj , by 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 poddrzewie 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 skorzystać z czystych przewodów. 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 numerycznej pracownika, wywołując metodę calculate zdefiniowaną w 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 będzie wykonywać żadnych ponownych obliczeń, chyba że zostanie zaktualizowana wartość liczbowa 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 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. 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ć, 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

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. 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.