Jak poprawić wydajność swojej aplikacji Angular
Opublikowany: 2020-04-10Mówiąc o najlepszych frameworkach frontendowych nie sposób nie wspomnieć o Angularze. Wymaga to jednak od programistów dużego wysiłku, aby się go nauczyć i mądrze z niego korzystać. Niestety istnieje ryzyko, że programiści, którzy nie mają doświadczenia w Angularze, mogą nieefektywnie wykorzystywać niektóre jego funkcje.
Jedną z wielu rzeczy, nad którymi zawsze musisz pracować jako programista frontendu, jest wydajność aplikacji. Duża część moich wcześniejszych projektów koncentrowała się na dużych aplikacjach korporacyjnych, które wciąż są rozbudowywane i rozwijane. Frameworki frontendowe byłyby tutaj niezwykle przydatne, ale ważne jest, aby używać ich poprawnie i rozsądnie.
Przygotowałem krótką listę najpopularniejszych strategii zwiększania wydajności i wskazówek, które mogą pomóc w natychmiastowym zwiększeniu wydajności aplikacji Angular . Należy pamiętać, że wszystkie podane tutaj wskazówki dotyczą Angulara w wersji 8.
ChangeDetectionStrategy i ChangeDetectorRef
Change Detection (CD) to mechanizm Angulara do wykrywania zmian danych i automatycznego reagowania na nie. Możemy wymienić podstawowe rodzaje standardowych zmian stanu aplikacji:
- Wydarzenia
- Żądanie HTTP
- Zegary
Są to interakcje asynchroniczne. Pytanie brzmi: skąd Angular miałby wiedzieć, że zaszły jakieś interakcje (takie jak kliknięcie, interwał, żądanie http) i jest potrzeba aktualizacji stanu aplikacji?
Odpowiedzią jest ngZone , który jest w zasadzie złożonym systemem przeznaczonym do śledzenia interakcji asynchronicznych. Jeśli wszystkie operacje są rejestrowane przez ngZone, Angular wie, kiedy zareagować na niektóre zmiany. Nie wie jednak, co dokładnie się zmieniło, i uruchamia mechanizm wykrywania zmian , który sprawdza wszystkie komponenty w pierwszej kolejności.
Każdy komponent w aplikacji Angular ma swój własny Change Detector, który określa, jak ten komponent powinien działać po uruchomieniu Change Detection – na przykład, jeśli istnieje potrzeba ponownego renderowania DOM komponentu (co jest dość kosztowną operacją). Gdy Angular uruchamia Wykrywanie zmian, każdy pojedynczy komponent zostanie sprawdzony, a jego widok (DOM) może zostać domyślnie ponownie wyrenderowany.
Możemy tego uniknąć, używając ChangeDetectionStrategy.OnPush:
@Składnik({ selektor: 'foobar', templateUrl: './foobar.component.html', styleUrls: ['./foobar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush })
Jak widać w powyższym przykładzie, musimy dodać dodatkowy parametr do dekoratora komponentu. Ale jak naprawdę działa ta nowa strategia wykrywania zmian?
Strategia mówi Angularowi, że określony komponent zależy tylko od jego @Inputs(). Ponadto, wszystkie komponenty @Inputs() będą działać jak niezmienny obiekt (np. gdy zmienimy tylko właściwość w @Input() obiektu, bez zmiany referencji, ten komponent nie będzie sprawdzany). Oznacza to, że wiele niepotrzebnych kontroli zostanie pominiętych i powinno to zwiększyć wydajność naszej aplikacji.
Komponent z ChangeDetectionStrategy.OnPush będzie sprawdzany tylko w następujących przypadkach:
- Odwołanie @Input() ulegnie zmianie
- Zdarzenie zostanie wyzwolone w szablonie komponentu lub jednym z jego dzieci
- Obserwowane w komponencie wywoła zdarzenie
- CD zostanie uruchomione ręcznie za pomocą usługi ChangeDetectorRef
- w widoku używany jest potok asynchroniczny (rurka asynchroniczna oznacza komponent do sprawdzenia pod kątem zmian – gdy strumień źródłowy wyemituje nową wartość, ten komponent zostanie sprawdzony)
Jeśli żadna z powyższych sytuacji nie wystąpi, użycie ChangeDetectionStrategy.OnPush wewnątrz określonego komponentu spowoduje, że komponent i wszystkie zagnieżdżone komponenty nie zostaną sprawdzone po uruchomieniu płyty CD.
Na szczęście nadal możemy mieć pełną kontrolę nad reagowaniem na zmiany danych za pomocą usługi ChangeDetectorRef. Musimy pamiętać, że przy ChangeDetectionStrategy.OnPush wewnątrz naszych limitów czasu, żądań, wywołań zwrotnych subskrypcji, musimy ręcznie odpalić CD, jeśli naprawdę tego potrzebujemy:
licznik = 0; konstruktor(private changeDetectorRef: ChangeDetectorRef) {} ngOnInit() { setTimeout(() => { to.licznik += 1000; this.changeDetectorRef.detectChanges(); }, 1000); }
Jak widać powyżej, wywołując this.changeDetectorRef.detectChanges() w naszej funkcji timeout , możemy ręcznie wymusić CD . Jeśli licznik zostanie w jakikolwiek sposób użyty wewnątrz szablonu, jego wartość zostanie odświeżona.
Ostatnia wskazówka w tej sekcji dotyczy trwałego wyłączania CD dla określonych komponentów. Jeśli mamy komponent statyczny i jesteśmy pewni, że jego stan nie powinien się zmieniać, możemy na stałe wyłączyć CD :
this.changeDetectorRef.detach()
Ten kod powinien zostać wykonany wewnątrz metody cyklu życia ngAfterViewInit() lub ngAfterViewChecked(), aby upewnić się, że nasz widok został poprawnie wyrenderowany, zanim wyłączymy odświeżanie danych. Ten komponent nie będzie już sprawdzany podczas CD , chyba że ręcznie uruchomimy metodę detectChanges().
Wywołania funkcji i gettery w szablonie
Użycie wywołań funkcji wewnątrz szablonów powoduje wykonanie tej funkcji za każdym razem, gdy działa detektor zmian. Ta sama sytuacja ma miejsce z getterami . Jeśli to możliwe, powinniśmy starać się tego uniknąć. W większości przypadków nie musimy wykonywać żadnych funkcji wewnątrz szablonu komponentu podczas każdego uruchomienia CD . Zamiast tego możemy użyć czystych rur.
Czyste rury
Rury czyste to rodzaj rur, których wyjście zależy tylko od jego wejścia, bez skutków ubocznych. Na szczęście wszystkie rury w Angularze są domyślnie czyste.
@Rura({ nazwa: 'wielkie litery', czysty: prawda })
Ale dlaczego mielibyśmy unikać używania rur z czystym: false? Odpowiedzią jest ponownie wykrywanie zmian. Potoki, które nie są czyste, są wykonywane przy każdym uruchomieniu CD, co w większości przypadków nie jest konieczne i zmniejsza wydajność naszej aplikacji. Oto przykład funkcji, którą możemy zmienić na czystą rurę:
transform(wartość: ciąg, limit = 60, wielokropek = '...') { if (!wartość || wartość.długość <= limit) { zwracana wartość; } const numberOfVisibleCharacters = value.substr(0, limit).lastIndexOf(' '); return `${value.substr(0, numberOfVisibleCharacters)}${wielokropek}`; }
Zobaczmy widok:
<p class="description">obcinaj (tekst, 30)</p>
Powyższy kod reprezentuje czystą funkcję – brak efektów ubocznych, wyjście zależne tylko od danych wejściowych. W takim przypadku możemy po prostu zastąpić tę funkcję przez pure pipe :
@Rura({ nazwa: 'obciąć', czysty: prawda }) klasa eksportu TruncatePipe implementuje PipeTransform { transform(wartość: ciąg, limit = 60, wielokropek = '...') { ... } }
I na koniec w tym widoku otrzymujemy kod, który zostanie wykonany tylko wtedy, gdy tekst zostanie zmieniony, niezależnie od Wykrywania zmian .
<p class="description">{{ tekst | skróć: 30 }}</p>
Lazy loading i preloading modułów
Jeśli Twoja aplikacja ma więcej niż jedną stronę, zdecydowanie powinieneś rozważyć tworzenie modułów dla każdego logicznego elementu Twojego projektu, zwłaszcza modułów z leniwym ładowaniem . Rozważmy prosty kod routera Angular:

const trasy: Trasy = [ { ścieżka: '', komponent: HomeComponent }, { ścieżka: 'foo', loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule) }, { ścieżka: 'bar', loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule) } ] @NgModule({ eksporty: [Moduł Routera], importy: [RouterModule.forRoot(trasy)] }) class AppRoutingModule {}
W powyższym przykładzie widzimy, że fooModule ze wszystkimi jego zasobami zostanie załadowany tylko wtedy, gdy użytkownik spróbuje wprowadzić konkretną trasę (foo lub bar). Angular wygeneruje również osobną porcję dla tego modułu . Lazy loading zmniejszy początkowe obciążenie .
Możemy przeprowadzić dalszą optymalizację. Załóżmy, że chcemy, aby moduły ładujące naszą aplikację działały w tle. W tym przypadku możemy skorzystać z preloadingStrategy. Angular domyślnie ma dwa rodzaje preloadingStrategy:
- Bez wstępnego ładowania
- Wczytaj wszystkie moduły
W powyższym kodzie domyślnie stosowana jest strategia NoPreloading. Aplikacja zaczyna ładować określony moduł na żądanie użytkownika (gdy użytkownik chce zobaczyć określoną trasę). Możemy to zmienić, dodając do routera dodatkową konfigurację.
@NgModule({ eksporty: [Moduł Routera], importy: [RouterModule.forRoot(trasy, { preloadingStrategy: PreloadAllModules }] }) class AppRoutingModule {}
Ta konfiguracja powoduje, że bieżąca trasa jest wyświetlana tak szybko, jak to możliwe, a następnie aplikacja spróbuje załadować inne moduły w tle. Sprytne, prawda? Ale to nie wszystko. Jeśli to rozwiązanie nie odpowiada naszym potrzebom, możemy po prostu napisać własną , niestandardową strategię .
Załóżmy, że chcemy wstępnie wczytać tylko wybrane moduły, na przykład BarModule. Wskazujemy to, dodając dodatkowe pole dla pola danych.
const trasy: Trasy = [ { ścieżka: '', komponent: HomeComponent dane: { wstępne wczytywanie: fałszywe } }, { ścieżka: 'foo', loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule), dane: { wstępne wczytywanie: fałszywe } }, { ścieżka: 'bar', loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule), dane: { wstępne wczytywanie: prawda } } ]
Następnie musimy napisać naszą niestandardową funkcję wstępnego ładowania:
@Wstrzykiwalny() export class CustomPreloadingStrategy implementuje PreloadingStrategy { preload(trasa: Trasa, wczytywanie: () => Obserwowalny<dowolny>): Obserwowalny<dowolny> { return route.data && route.data.preload ? load() : z(null); } }
I ustaw jako preloadingStrategy:
@NgModule({ eksporty: [Moduł Routera], importy: [RouterModule.forRoot(trasy, { preloadingStrategy: CustomPreloadingStrategy }] }) class AppRoutingModule {}
Na razie tylko trasy z param { data: { preload: true } } będą wstępnie ładowane. Pozostałe trasy będą działać tak, jak ustawione jest NoPreloading.
Niestandardowa preloadingStrategy to @Injectable(), co oznacza, że jeśli zajdzie taka potrzeba, możemy wstrzyknąć niektóre usługi i dostosować naszą preloadingStrategy w jakikolwiek inny sposób.
Dzięki narzędziom programistycznym przeglądarki możemy zbadać wzrost wydajności dzięki równemu początkowemu czasowi ładowania z preloadingStrategy i bez niego. Możemy również spojrzeć na zakładkę sieci, aby zobaczyć, że fragmenty innych tras ładują się w tle, podczas gdy użytkownik może zobaczyć bieżącą stronę bez żadnych opóźnień.
funkcja trackBy
Możemy założyć, że większość aplikacji Angulara używa *ngFor do iteracji elementów wymienionych w szablonie. Jeśli iterowana lista jest również edytowalna, trackBy jest absolutnie koniecznością.
<ul> <tr *ngFor="let produkt produktów; trackBy: trackByProductId"> <td>{{ product.title }}</td> </tr> </ul> trackByProductId(indeks: numer, produkt: produkt) { zwrot produktu.id; }
Dzięki funkcji trackBy Angular jest w stanie śledzić, które elementy kolekcji uległy zmianie (według podanego identyfikatora) i ponownie renderować tylko te konkretne elementy. Gdy pominiemy trackBy, cała lista zostanie ponownie załadowana, co może być bardzo zasobożerną operacją na DOM.
Kompilacja z wyprzedzeniem (AOT)
Odnośnie dokumentacji Angulara:
„ (…) komponenty i szablony dostarczane przez Angular nie mogą być zrozumiane bezpośrednio przez przeglądarkę, aplikacje Angular wymagają procesu kompilacji, zanim będą mogły zostać uruchomione w przeglądarce ”
Angular udostępnia dwa rodzaje kompilacji:
- Just-in-Time (JIT) – kompiluje aplikację w przeglądarce w czasie wykonywania
- Ahead-of-Time (AOT) – kompiluje aplikację w czasie kompilacji
Do użytku programistycznego kompilacja JIT powinna pokrywać potrzeby programistów. Niemniej jednak do buildów produkcyjnych zdecydowanie powinniśmy używać AOT . Musimy upewnić się, że flaga aot w pliku angular.json jest ustawiona na true. Najważniejsze zalety takiego rozwiązania to szybsze renderowanie, mniej asynchronicznych żądań, mniejszy rozmiar pobierania frameworka i zwiększone bezpieczeństwo.
Streszczenie
Wydajność aplikacji to coś, o czym musisz pamiętać zarówno podczas tworzenia, jak i utrzymania projektu. Jednak samodzielne poszukiwanie możliwych rozwiązań może być czasochłonne i pracochłonne. Sprawdzanie tych często popełnianych błędów i pamiętanie o nich podczas procesu tworzenia nie tylko pomoże ci w krótkim czasie poprawić wydajność aplikacji Angular, ale także pomoże uniknąć przyszłych potknięć.
Zaufaj Miquido swoim projektom Angular
Skontaktuj się z namiChcesz stworzyć aplikację z Miquido?
Myślisz o zwiększeniu rozwoju swojej firmy dzięki aplikacji Angular? Skontaktuj się z nami i wybierz nasze usługi tworzenia aplikacji Angular.