23 lip 2018
5 min

Angular performance tips

Performance w aplikacjach angularowych to już mocno przeorany i zbadany temat, tym niemniej od dawna nosiłem się z zamiarem zebrania wszystkich trików które poznałem w jeden spis. Brzmi interesująco? 🙂 to kontynuujemy!

1. Strategia OnPush

Najbardziej popularna metoda poprawy wydajności. W skrócie polega na tym, że w komponencie zostanie uruchomiony system detekcji dopiero wtedy, gdy w @Input() zmieni się referencja do obiektu do którego się odnosi, lub z templatki zostanie wyemitowany event.

@Component({
  ...
  changeDetection: ChangeDetectionStrategy.OnPush
})

W celu lepszego zapoznania OnPush i tego jak działa system detekcji, zapraszam do przeczytania artykułu:
http://wp.angular.love/2017/01/15/angular-2-change-detector-mechanizmy-detekcji-oraz-strategia-onpush/

2. LazyLoading modułów

Stworzyłeś/aś aplikację, w której użytkownik może założyć konto i się logować, a sama aplikacja już spuchła od funkcjonalności? No to po co serwować od razu całą aplikację? Warto rozważyć ładowanie modułów na żądanie, a na starcie załadować np. tylko moduł z logowaniem. W Angularze jest to bardzo proste i można to wykonać na poziomie definiowania routingu:

const routes: Routes = [
  {
    path: 'customers',
    loadChildren: 'app/customers/customers.module#CustomersModule',
    canLoad: [AuthGuard]
  }
];

LazyLoading współgra z guardem CanLoad, który zapobiega ładowaniu lazy modułów, jeśli użytkownik nie jest autoryzowany.

Dokumentacja bardzo dobrze opisuje jak korzystać z Lazy Loading, polecam!
https://angular.io/guide/lazy-loading-ngmodules

3. ngZones

Dzięki mechanizmowi Zones, Angular wie kiedy ma uruchomić wykrywanie zmian (Change Detection). Warto korzystać z ngZone.runOutsideAngular gdy wielokrotnie wykonujemy powtarzającą się asynchronicznie operację, po której nie mamy potrzeby odświeżania UI (np. nasłuchujemy na event scroll obiektu Window, w celu np. zmiany stylu jakiegoś elementu).

Zachęcam do przeczytania kompleksowego wpisu na temat Zones:

http://wp.angular.love/2018/03/04/angular-i-zone-js/

4. ChangeDetectorRef

Angular pozwala na ręczne sterowanie systemem detekcji. ChangeDetectorRef często jest wykorzystywany wraz ze strategią OnPush, w momencie gdy sami chcemy uruchomić system detekcji w komponencie.

ChangeDetectorRef posiada min. metodę detach():

constructor(private changeDetector: ChangeDetectorRef) {
  this.changeDetector.detach();
}

która odpina ChangeDetector komponentu od drzewa ChangeDetectors (ale w komponencie cały czas działa system detekcji). Następnie w dowolnym momencie możemy sami wołać CD, za pomocą metody detectChanges (sprawdzenie komponentu i jego dzieci) lub markForCheck (sprawdzenie od app root aż do naszego komponentu).

5. PurePipes

Wszystkie pipes  w Angular są domyślnie pure, czyli uruchomią się kolejny raz tylko w przypadku, jeśli w wartości którą przyjęły, zmieniła się referencja do obiektu lub po prostu wartość. Spójrzmy na poniższy kod:

<label>{{ getLabelFor(productSerial) }}</label>

Powyżej, funkcja getLabelFor będzie wołana za każdym razem, gdy uruchomi się system detekcji w wyniku którego, odswieżą się bindingi na widoku (czyli wąsy {{ … }}  :)). A że system detekcji uruchamia się bardzo często w Angularze, to nasza funkcja będzie niepotrzebnie wołana wielokrotnie.

W tej sytuacji lepiej stworzyć pipe „ProductSerialLabelPipe„, który po prostu zwróci odpowiednią labelkę dla danej wartości i będzie wykorzystany następująco:

<label>{{ productSerial | productSerialLabel }}</label>

6. @Input() setter zamiast hooku ngOnChanges.

Angular udostępnia nam hook ngOnChanges, który uruchamia się za każdym razem, gdy do @Input trafi nowa wartość. Problem w tym, że uruchomienie nastąpi zawsze, gdy zmieni się jakikolwiek @Input, co już nie jest zbyt optymalne, gdy komponent ma wiele Inputs. Jeśli chcesz odpalić jakiś kod gdy konkretny input dostanie wartość, to lepiej zrobić to poprzez setter:

@Input() set someInput(value) {
  this.runSomeFunction(); // wywoła się w przypadku, gdy someInput otrzyma nową wartość
}

Niż poprzez ngOnChanges:

ngOnChanges(changes) {
  this.runSomeFunction(); // wywoła się zawsze gdy jakikolwiek @Input otrzyma nową wartość
}

TIP od czytelnika Łukasz Pawełczak w kontekście stosowania setterów:

W przypadku stosowania setterów, które polegają na sobie nawazajem, należy uważać na kolejność ich deklaracji, gdyż w właśnie w takiej kolejności się wywołują.

Dzięki Paweł za dobry tip!

7. Niestosowanie złożonych operacji w hooku ngDoCheck

W Angularze dostępny jest hook ngDoCheck, który uruchamia się za każdym razem gdy uruchomi się system detekcji oraz raz po ngOnInit. Z tego względu, warto unikać jakichś złożonych obliczeń w ngDoCheck.

Polecam również mój wpis o ngOnChanges vs NgDoCheck:

http://wp.angular.love/2017/01/23/angular-2-lifecycle-hooks-ngonchanges-ngoncheck/

8. TrackBy dla *ngFor

Załóżmy, że wyświetlamy złożoną tabelę z dużą ilością wierszy, która jednocześnie jest edytowalna (np. można usunąć wiersz). W przypadku zmiany danych i referencji tablicy z danymi, Angular przerenderuje cały DOM z tabelką na nowo. Spójrzmy na przykład:

<table>
  <tr *ngFor="let car of cars; trackBy: trackByCarId">
    <td>{{ car.id }}</td>
  </tr>
</table>

Powyżej, zaraz za kolekcją cars w *ngFor, umieściłem trackBy: trackByCarId. Do trackBy przekazujemy funkcję:

trackByCarId(index: number, car: Car) {
  return car.id; // lub index
}

Teraz Angular może śledzić, które elementy kolekcji zostały dodane lub usunięte zgodnie z unikalnym identyfikatorem (tutaj car id) i np. niszczyć tylko te węzły DOM, które faktycznie powinny zniknąć.

9. Preloading modułów

Oprócz leniwego ładowania modułów, możemy również zastosować preloading modułów, który załaduje nieużywane moduły asynchronicznie w tle podczas wystartowania aplikacji, tak aby użytkownik jak najszybciej mógł zobaczyć widok. Angular domyślnie nie korzysta z preloadingu. Możemy zastosować preloading na wszystkie lazy modules, lub napisać własną strategię, którą przekażemy w konfiguracji Routera:

RouterModule.forRoot(routes, { 
   preloadingStrategy: PreloadAllModules // lub nasze AppCustomPreloadingStrategy
})

Tutaj guide: https://angular.io/guide/router#preloading-background-loading-of-feature-areas

10. Memoizacja

Nigdy nie korzystałem z memoizacji, zobaczyłem ten trik pierwszy raz na występie Nir Kaufmana na NgPoland 2017.

Memoizacja – technika optymalizacji, która polega na zapisywaniu w pamięci rezultatów wywołań kosztowych funkcji i zwracaniu „skeszowanych” wyników, gdy funkcja zostanie znowu zawołana z tym samym inputem.

Jak skorzystać? najatwiej poprzez dekoratory z biblioteki Lodash:

@import { memoize } form 'lodash-decorators';
...
@memoize()
getHeavyOperation(value) {
  // return heavy operation with value  
}
...

Teraz, gdy getHeavyOperation zostanie zawołany z tym samym parametrem, aplikacja skorzysta z rezultatu, który już wcześniej przeliczyła i zwróciła, zamiast robić to na nowo.

ADNOTACJA: jednak lodashowy Memoize, nie jest taki dobry 😉 Dostałem informację od czytelnika bloga, Tomka Janiszewskiego z poniższymi uwagami do @Memoize z Lodasha:

1. Tylko pierwszy parametr memoizowanej funkcji jest brany pod uwagę.
2.  Parametr ten jest porównywany po referencji
W celu eliminacji powyższych zachowań, dobrodziej Tomek Janiszewski stworzył własną paczkę z dekoratorem :):
https://www.npmjs.com/package/memoize-object-decorator

11. UpdateOnBlur

Angular domyślnie uruchamia proces walidacji za każdym razem, gdy zmieni się wartość w FormControl, więc wiele callbacków jest uruchamianych z każdą wpisaną literką np. do <input>. Możemy uruchomić walidację dopiero wtedy, gdy użytkownik straci focus na kontrolce (blur event):

this.email = new FormControl(null, { updateOn: 'blur' });

Można updateOn również zastosować na FormGroup, lub skorzystać z updateOn: 'submit’ (czyli uruchomić walidację dopiero wtedy, gdy użytkownik zatwierdzi formularz).

12. Interfejsy zamiast klas

Praca w Angularze łączy się z typowaniem naszego kodu. Pamiętaj aby do typowania używać interfejsów, które są usuwane z kodu wynikowego (zmiejszamy bundle size aplikacji!), w przeciwieństwie do klas!
ŹLE:

class Person {
  name: string;
}

DOBRZE:

interface Person {
  name: string;
}

Używanie klas do typowania także błędnie sugeruje, że może powstać instancja tej klasy.

13. Caching zapytań HTTP

Warto zapisywać w pamięci wyniki zapytań HTTP, które na pewno się nie zmienią w trakcie działania aplikacji (np. słowników). Można to robić chociażby poprzez NgRx Store lub własny serwis. Ciekawe rozwiązanie również pod poniższym linkiem:

https://blog.thoughtram.io/angular/2018/03/05/advanced-caching-with-rxjs.html

14. AOT Build

W Angular rozrózniamy dwa typy buildów:

  • JIT (Just In Time) – aplikacja kompiluje się w czasie runtime, w przeglądarce, nadaje się do lokalnego developmentu
  • AOT (Ahead Of Time) – aplikacja jest już skompilowana w czasie buildu, idealny na wersję produkcyjną aplikacji

Z JIT korzystamy uruchamiając aplikację poprzez komendy ng build oraz ng serve, natomiast z AOT komendami ng build –aot, ng serve –aot, oraz ng build –prod.

Zalety AOT:

  • szybsze renderowanie, przeglądarka pobiera pre-skompilowaną wersję aplikacji, więc może od razu renderować widok
  • HTML i CSS są już dorzucone do plików JS, stąd mniej zapytań HTTP
  • mniejszy bundle size, nie ma potrzeby pobierać Angular Compilera (niemalże połowa rozmiaru frameworka Angular!), no bo przecież aplikacja jest już skompilowana
  • wykrycie błędów w templatkach już na etapie build
  • lepsze security, gdyż HTML jest zaszyty w plikach JS

15. Użycie odpowiednich operatorów RxJS

Angular uruchamia system detekcji po każdej, asynchronicznej operacji. Czyli także wtedy, gdy Observable wyemituje nową wartość. W pewnych sytuacjach, możemy zadbać o to, aby wartości nie były za często emitowane.

Np. w przypadku korzystania z Observable formControl.valueChanges, który pozwala nasłuchiwać na to co użytkownik wstukuje do kontrolki,  można skorzystać z operatorów debounce i distinctUntilChanges, w celu ograniczenia ilości emitowanych wartości:

formControl.valueChanges
  .pipe(debounceTime(350), distinctUntilChanged())
  ._subscribe(...);
  • debounceTime – opóźnia wyemitowanie wartości o zadany czas, jednocześnie porzuca wszystkie poprzednio zakolejkowane wartości do emisji
  • distinctUntilChanged – wartość zostanie wyemitowana tylko wtedy, gdy jest inna niż poprzednia.

Warto również łączyć strumienie, jeśli chcemy obsługiwać wartości, niezależnie z którego strumienia wyszły:

merge(formControlA.valueChanges, formControlB.valueChanges)
  .subscribe(...)

Strumienie łączymy za pomocą funkcji merge, dzięki temu w powżyszym przypadku możemy skorzystać z jednego obsevera.

16. SSR – Server Side Rendering

SSR pozwala poprawić SEO oraz szybkość załadowania aplikacji, dzięki temu, że żądana strona jest pre-renderowana już na serwerze i markup strony jest dostarczany podczas początkowego ładowania strony.

Angular posiada dedykowaną bibliotekę do SSR – Angular Universal.

17. ServiceWorker

Poprawa performance poprzez przechwytywanie zapytań HTTP wychodzących ze strony klienta przez Service Worker, który decyduje co dalej z nimi zrobić. Więcej info w docsach:

https://angular.io/guide/service-worker-intro

Podsumowanie

Warto się zastanowić, z ilu trików chcemy korzystać. Zdarza się, że porzucam strategię OnPush, jeśli w wielu miejscach musiałbym uruchamiać ręcznie system detekcji poprzez wstrzykiwanie ChangeDetectorRef i wołanie detectChanges, a sam komponent działa np. na tylko jednym inpucie i nie ma komponentów dzieci. Tabela ma 4 wiersze? odpuść sobie TrackBy. Aplikacja ma dwa, malutkie moduły i wszystko bardzo szybko się ładuje? Olałbym lazy loading.

No pewno dobrze być świadomym wielu możliwości Angulara w przypadku poprawy performance ale trzeba korzystać z nich głową, aby za bardzo sobie nie utrudniać życia i wyciągać z nich faktyczną korzyść ;). Jeśli aplikacja zamula Ci na froncie, to mam nadzieję, że któryś z powyższych tipów pomoże!

Spodobał się artykuł? To kliknij po lewej w menu przycisk „Like” dla bloga i bądź na bieżąco ;)!

Podziel się artykułem

Zapisz się na nasz newsletter

Dołącz do community Angular.love i bądź na bieżąco z trendami.