Optymalizowanie wykrywania zmian w Angular

Szybsze wykrywanie zmian dla wygody użytkowników.

Angular okresowo uruchamia mechanizm wykrywania zmian, aby zmiany modelu danych były odzwierciedlane w widoku aplikacji. Wykrywanie zmian może być uruchamiane ręcznie lub przez zdarzenie asynchroniczne (np. interakcję użytkownika lub zakończenie XHR).

Wykrywanie zmian to zaawansowane narzędzie, ale jeśli jest bardzo często używane, 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, pomijając niektóre elementy aplikacji i uruchamiając wykrywanie zmian tylko wtedy, gdy jest to konieczne.

Wykrywanie zmian w Angular

Aby zrozumieć, jak działa wykrywanie zmian w Angular, spójrzmy na przykładową aplikację.

Kod aplikacji znajdziesz w tym repozytorium na GitHubie.

Aplikacja zawiera listę pracowników z dwóch działów firmy – sprzedaży oraz badań i rozwoju. Składa się z 2 elementów:

  • AppComponent, który jest głównym komponentem aplikacji,
  • 2 instancje EmployeeListComponent, 1 na potrzeby sprzedaży i 1 do celów badawczo-rozwojowych.

Przykładowa aplikacja

W szablonie dla elementu AppComponent możesz zobaczyć 2 wystąpienia parametru 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 w obliczeniach biznesowych i wizualizuje wynik na ekranie.

Spójrz teraz 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 wywoła odpowiednie dane wyjściowe. Komponent określa 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 jest powtarzany nad wszystkimi pracownikami na liście i dla każdego z nich renderuje element listy. Zawiera też dyrektywę ngModel dotyczącą dwukierunkowego wiązania danych między danymi wejściowymi a właściwością label zadeklarowaną w pliku EmployeeListComponent.

Przy 2 instancjach EmployeeListComponent aplikacja tworzy to drzewo komponentów:

Drzewo komponentów

AppComponent to główny komponent aplikacji. Jej komponenty podrzędne to 2 wystąpienia obiektu EmployeeListComponent. Każda instancja zawiera listę elementów (E1, E2 itp.) reprezentujących poszczególnych pracowników w dziale.

Gdy użytkownik zacznie wpisywać imię i nazwisko nowego pracownika w polu do wprowadzania danych EmployeeListComponent, Angular aktywuje wykrywanie zmian w całym drzewie komponentów, począwszy od AppComponent. Oznacza to, że gdy użytkownik wpisuje tekst, Angular wielokrotnie przelicza wartości liczbowe poszczególnych pracowników, aby sprawdzić, czy nie zmieniły się od ostatniej kontroli.

Aby zobaczyć, jak bardzo to działa, otwórz niezoptymalizowaną wersję projektu w StackBlitz i wpisz imię i nazwisko pracownika.

Aby sprawdzić, czy spowolnienie używa 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ść.

, 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, proces ten może zablokować wątek UI przeglądarki i spowodować spadek klatek, co źle wpływa na wrażenia użytkownika.

Pomijam poddrzewa komponentów

Gdy użytkownik wpisuje tekst sprzedaży EmployeeListComponent, wiesz, że dane w dziale R&D się nie zmieniają, więc nie ma powodu, by uruchamiać wykrywanie zmian w jego komponencie. Aby upewnić się, że instancja badawczo-rozwojowa nie uruchamia wykrywania zmian, ustaw wartość changeDetectionStrategy w polu 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 dla odpowiedniego działu:

Wykrywanie zmian w poddrzewie komponentu

Ta optymalizacja zastosowana do pierwotnej aplikacji znajdziesz tutaj.

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

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

Używanie czystych rur

Mimo że strategia wykrywania zmian w EmployeeListComponent ma teraz ustawienie OnPush, Angular nadal oblicza ponownie wartości liczbowe dla wszystkich pracowników działów, gdy użytkownik wpisze odpowiednie dane tekstowe.

Aby poprawić to działanie, możesz użyć czystych kresek. Zarówno rury czyste, jak i nieczyste przyjmują dane wejściowe i wyniki, których można używać w szablonie. Różnica między nimi polega na tym, że czysty potok obliczy ponownie swój 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 EmployeeListComponent. Jeśli przeniesiesz obliczenia do zwykłej pionowej kreski, Angular obliczy to wyrażenie ponownie tylko wtedy, gdy zmienią się jego argumenty. Platforma określi, czy argumenty potoku zmieniły się, przeprowadzając sprawdzenie referencji. Oznacza to, że Angular nie będzie wykonywać żadnych ponownych obliczeń, dopóki nie zaktualizujesz wartości liczbowej pracownika.

Aby przenieść obliczenia biznesowe do potoku 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 rurka jest czysta. Jeśli nie określisz inaczej, Angular uzna wszystkie rury za czyste.

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 widać, o ile płynniejsze jest pisanie.

Aby zobaczyć efekt ostatniej optymalizacji, spróbuj zastosować ten przykład do StackBlitz.

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

Podsumowanie

W przypadku spowolnienia działania aplikacji Angular:

  1. Profiluj aplikację za pomocą Narzędzi deweloperskich w Chrome, aby sprawdzić, skąd pochodzą spowalnianie działania aplikacji.
  2. Wprowadź strategię wykrywania zmian OnPush, aby przycinać poddrzewa komponentu.
  3. Przenieś ciężkie obliczenia do czystych potoków, aby umożliwić platformie buforowanie obliczonych wartości.