Optymalizowanie wykrywania zmian w Angular

Wdrożenie szybszego wykrywania zmian w celu poprawy komfortu użytkowników.

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

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

Z tego posta 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 Angularze, 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 badań i rozwoju – i składa się z 2 komponentów:

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

Przykładowa aplikacja

W szablonie dla AppComponent zobaczysz 2 wystąpienia symbolu 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 imię i nazwisko oraz wartość liczbową. Aplikacja przekazuje wartość liczbową pracownika do obliczeń biznesowych i wyświetla 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 przyjmuje jako dane wejściowe listę pracowników i nazwę działu. Gdy użytkownik próbuje usunąć lub dodać pracownika, komponent wywołuje 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 iteruje po wszystkich pracownikach na liście i dla każdego z nich renderuje element listy. Zawiera też dyrektywę ngModel do dwukierunkowego wiązania danych między polem wejściowym a właściwością label zadeklarowaną w EmployeeListComponent.

Dzięki 2 instancjom elementu EmployeeListComponent aplikacja tworzy ten drzewo komponentów:

Drzewo komponentów

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

Gdy użytkownik zacznie wpisywać w polu wprowadzania wEmployeeListComponent imię i nazwisko nowego pracownika, Angular uruchomi wykrywanie zmian w całym drzewie komponentów, zaczynając od AppComponent. Oznacza to, że gdy użytkownik wpisuje tekst, Angular wielokrotnie przelicza wartości liczbowe powiązane z każdym pracownikiem, aby sprawdzić, czy nie uległy one zmianie od ostatniego sprawdzenia.

Aby zobaczyć, jak wolne może być działanie aplikacji, otwórz niezoptymalizowaną wersję projektu w StackBlitz i spróbuj wpisać imię i nazwisko pracownika.

Możesz sprawdzić, czy spowolnienie wynika z funkcji fibonacci, konfigurując przykładowy projekt i otwierając kartę Wydajność w Narzędziach deweloperskich w Chrome.

  1. Aby otworzyć Narzędzia dla programistów, naciśnij Ctrl+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 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 przeglądarki i spowodować utratę klatek, co pogorszy wrażenia użytkownika.

Pomijanie poddrzew komponentów

Gdy użytkownik wpisuje tekst w polu sprzedaż EmployeeListComponent, wiesz, że dane w dziale B+R nie ulegają zmianie, więc nie ma powodu, aby uruchamiać wykrywanie zmian w jego komponencie. Aby mieć pewność, że instancja R&D nie wywoła 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 w polu tekstowym, wykrywanie zmian zostanie wywołane tylko w przypadku odpowiedniego działu:

Wykrywanie zmian w poddrzewie komponentu

Zoptymalizowaną wersję oryginalnej aplikacji znajdziesz tutaj.

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

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

Używanie czystych funkcji potokowych

Nawet jeśli strategia wykrywania zmian dla EmployeeListComponent jest teraz ustawiona na OnPush, Angular nadal przelicza wartość liczbową dla wszystkich pracowników w dziale, gdy użytkownik wpisuje odpowiedni tekst.

Aby poprawić to działanie, możesz skorzystać z czystych potoków. Zarówno czyste, jak i nieczyste przekształcenia przyjmują dane wejściowe i zwracają wyniki, które można wykorzystać w szablonie. Różnica między nimi polega na tym, że czysty potok ponownie oblicza wynik tylko wtedy, gdy otrzyma inne dane wejściowe niż w poprzednim wywołaniu.

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 czystego potoku, Angular ponownie obliczy wyrażenie potoku tylko wtedy, gdy zmienią się jego argumenty. Framework sprawdzi, czy argumenty potoku uległy zmianie, wykonując sprawdzenie odniesienia. Oznacza to, że Angular nie będzie wykonywać żadnych ponownych obliczeń, chyba że zostanie zaktualizowana wartość liczbowa pracownika.

Aby przenieść obliczenia dotyczące firmy 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 potok jest czysty. Angular uzna wszystkie przekształcenia 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 w polu powiązanym z dowolnym działem, aplikacja nie będzie ponownie obliczać wartości liczbowej dla poszczególnych pracowników.

W aplikacji poniżej możesz zobaczyć, jak płynne jest pisanie.

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

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

Podsumowanie

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

  1. Profiluj aplikację za pomocą Narzędzi deweloperskich w Chrome, aby sprawdzić, skąd pochodzą spowolnienia.
  2. Wprowadź strategię wykrywania zmian OnPush, aby przycinać poddrzewa komponentu.
  3. Przenieś złożone obliczenia do czystych funkcji potokowych, aby umożliwić platformie buforowanie obliczonych wartości.