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.
1 2 3 4 |
@Component({ ... changeDetection: ChangeDetectionStrategy.OnPush }) |
W celu lepszego zapoznania OnPush i tego jak działa system detekcji, zapraszam do przeczytania artykułu:
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:
1 2 3 4 5 6 7 |
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://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():
1 2 3 |
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:
1 |
<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:
1 |
<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:
1 2 3 |
@Input() set someInput(value) { this.runSomeFunction(); // wywoła się w przypadku, gdy someInput otrzyma nową wartość } |
Niż poprzez ngOnChanges:
1 2 3 |
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://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:
1 2 3 4 5 |
<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ę:
1 2 3 |
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:
1 2 3 |
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:
1 2 3 4 5 6 7 |
@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:
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):
1 |
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:
1 2 3 |
class Person { name: string; } |
DOBRZE:
1 2 3 |
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:
1 2 3 |
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:
1 2 |
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 ;)!
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
z tym, że simplesChanges z ngOnChanges otrzymujemy gdy jakikolwiek @Input się zmieni, tak jak w arcie napisałem, więc jest różnica:)
Dlatego należałoby sprawdzić czy w changes jest wartość z danego inputa – jeśli input się nie zmienił to w simpleChanges nie będzie pola z tym inputem 🙂
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 🙂
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.
Pingback: Angular Tips & Tricks cz. VII - Angular.love