Wróć do strony głównej
Angular

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.

W celu lepszego zapoznania OnPush i tego jak działa system detekcji, zapraszam do przeczytania artykułu:

ANGULAR 2 Change Detector

 

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:

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://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():

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:

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:

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:

Niż poprzez ngOnChanges:

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://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:

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

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:

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:

 

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):

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:

DOBRZE:

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:

  • 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:

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 ;)!

 

O autorze

Tomasz Nastały

JavaScript Developer, entuzjasta frameworka Angular oraz TypeScripta. Na co dzień lubię dzielić się wiedzą poprzez prowadzenie zajęć w jednym z trójmiejskich bootcampów i nagrywaniem kursów z Angulara.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

6 komentarzy

  1. Lukasz Ostrowski

    6. @Input() setter zamiast hooku ngOnChanges

    w sumie uzycie simpleChanges i porownanie wartosci inputa da podobny efekt. ale tez wole input setter, szybciej i czytelniej

  2. Tomek

    Bardzo ciekawy artykuł, jak i cały blog, który na bieżąco śledzę. Większość „trików” znałem, ale przyznam szczerze, że czasami potrzeba przypomnieć o paru kwestiach nie tyle jak je wykonać technicznie, ale żeby własnie o nich pamiętać 🙂 Czasami termin goni termin i człowiek zbyt mocno skupia się na pierdołach, zapominając o wydajności…
    Pozdrawiam i czekam na więcej 🙂

  3. Langus

    8. TrackBy dla *ngFor – gdy wywołujemy w *ngFor komponent z @Inputem, to i tak do tego @Inputa wstrzeliwana jest za każdym razem wartość (taka sama), co za tym idzie wywoływany jest hook ngOnChanges. Czyli może być za każdym razem strzał do naszego API z tymi samymi wartościami.
    Ja to obchodzę że przed pobraniem danych sprawdzam stan flagi this.dataLoaded != true, a po pobraniu danych ustawiam na true.

  4. Pingback: Angular Tips & Tricks cz. VII - Angular.love

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *