Szybszy Angular poprzez mniej pracy: dziesięć technik wykraczających poza OnPush i trackBy
Ogranicz marnotrawstwo zasobów sieciowych, reaktywne działanie, pracę DOM i CPU w nowoczesnym Angular dzięki @defer, stopniowej hydratacji, funkcji resource(), sygnałom obliczanym, fazom renderowania oraz Web Workers.
Źródła: How to Make Angular Extremely Fast: 10 Performance Tricks Most Developers Miss
Runtime Angulara rzadko jest przyczyną wolnego działania aplikacji. Znacznie częściej aplikacja ładuje, inicjuje, przelicza i renderuje elementy, których nikt nie zażądał. Znane rozwiązania, takie jak OnPush, ścieżki typu lazy, trackBy, kompilacja AOT, wersje produkcyjne oraz mniej wywołań HTTP, nadal pomagają, ale nowsze wersje Angulara dają znacznie lepszą kontrolę: kiedy ładuje się kod, kiedy komponenty stają się interaktywne, kiedy żądania są anulowane, co unieważnia stan aplikacji, kiedy wykonywane są operacje na DOM-ie i na którym wątku odbywają się obciążone obliczenia. Ten przewodnik przedstawia dziesięć technik opartych na tej kontroli, sposoby ich łączenia oraz kolejność stosowania po zbadaniu tego, gdzie faktycznie traci się czas.
Główna idea jest prosta: zamiast sprawiać, by Angular pracował szybciej przy niepotrzebnych zadaniach, należy zaaranżować wszystko tak, aby te zadania w ogóle nie miały miejsca.
Gdzie aplikacja Angular spędza swój czas
Zanim przejdziemy do poszczególnych API, przydatne jest wyobrażenie sobie trzech zasobów, które zużywa każda aplikacja Angular, oraz tego, co zwykle je konsumuje.
Angular Application
│
┌────────────────┼────────────────┐
│ │ │
Network CPU DOM
│ │ │
JS downloads Change detection Layout
API requests Computation Paint
Dependencies Parsing Reflow
│ │ │
└────────────────┼────────────────┘
│
User Experience
Te koszty wzajemnie na siebie wpływają. Optymalizacja detekcji zmian przynosi niewiele, jeśli przy pierwszym załadowaniu strony pobierane są 5 MB kodu JavaScript, a mały plik nie pomoże, jeśli każda interakcja wywołuje kosztowne obliczenia. Celowym podejściem jest: mniejsze pobieranie, mniej inicjalizacji, mniej obliczeń, mniej renderowania oraz mniejsze blokowanie głównej wątku. Dziesięć poniższych technik dotyczy każdego z tych kroków.
1. Spraw, aby @defer tworzył rzeczywiście oddzielne fragmenty
Blok @defer to jeden z najprostszych sposobów na zmniejszenie ilości JavaScriptu wymaganego przy uruchamianiu. Wyobraź sobie panel sterowania z wykresami, danymi analitycznymi, raportami, mapami, odtwarzaczami wideo oraz elementami administracyjnymi. Niewiele z tych elementów musi istnieć od razu po otwarciu strony, a mimo to prosty szablon ładuje je wszystkie natychmiastowo:
<app-heavy-chart />
<app-map />
<app-report />
Umieszczenie ciężkiego komponentu w bloku z opcją @defer mówi Angularowi, aby pobrał jego kod tylko w momencie wystąpienia określonego wyzwalacza, np. gdy element zastępczy pojawi się w polu widzenia:
@defer (on viewport) {
<app-heavy-chart />
}
Unikaj importowania komponentów z opcją @defer w imporcie typu barrel
Istnieje tu subtelny problem: @defer może służyć tylko do oddzielenia kodu, który nie jest już dostępny poprzez standardowy import. Jeśli komponent gospodarzy importuje komponent z opcją @defer za pośrednictwem pliku typu barrel, ten plik łączy ze sobą wszystkie inne elementy eksportowane oraz ich zależności w jeden graf. Dlatego zamiast importować z pliku index:
import { HeavyComponent } from './index';
Importuj komponent z jego własnego pliku:
import { HeavyComponent } from './heavy.component';
Celem jest utrzymanie grafu zależności za każdym blokiem odkładanym w jak największej izolacji. Pierwszy kształt, którego należy unikać, jest oznaczony:
BAD
i wygląda on w ten sposób, z pojedynczym łącznikiem łączącym każdy element interfejsu z panelami sterowania:
Dashboard
│
└── index.ts
├── Chart
├── Map
├── Reports
├── Video
└── Analytics
Wolany kształt jest oznaczony:
GOOD
i umożliwia bezpośredni import złożonego wykresu, dzięki czemu ma on swój własny fragment kodu:
Dashboard
│
└── HeavyChart
│
└── heavy-chart.chunk.js
Importy za pośrednictwem łączników są jedną z najczęstszych ukrytych przyczyn, dla których blok @defer nie udaje się zmniejszyć początkowego rozmiaru pliku. Najprostszym sposobem na potwierdzenie sukcesu jest sprawdzenie wyników kompilacji pod kątem nowego fragmentu kodu generowanego w trybie opóźnionym po dodaniu bloku.
Korzyści są kumulatywne: mniejszy początkowy pakiet oznacza mniej danych do pobrania, przetworzenia, skompilowania i uruchomienia, a cała ta oszczędność następuje jeszcze przed tym, jak Angular wyświetli cokolwiek.
2. Hydratuj tylko to, czego użytkownicy potrzebują najpierw
Przy renderowaniu po stronie serwera drugi wąskie gardło pojawia się po przybyciu HTML. Serwer może szybko wygenerować kod strony, ale przeglądarz musi następnie uczynić całą stronę interaktywną, co oznacza pobieranie i uruchamianie kodu dla każdego z jej komponentów. Rozważmy typową strukturę panelu sterowania:
Dashboard
│
├── Header
├── Navigation
├── KPI Cards
├── Huge Chart
├── Map
├── Analytics Table
├── Video Player
└── Recommendations
Nie wszystkie z tych sekcji muszą natychmiast reagować na dane wejściowe. Klasyczna pełna hydratacja traktuje stronę jako jedną całość:
SSR HTML
↓
Hydrate EVERYTHING
↓
Interactive
Hydratacja stopniowa pozwala podzielić to na etapy, dzięki czemu strona jest widoczna od razu, a sekcje stają się interaktywne stopniowo:
SSR HTML
↓
Show page immediately
↓
Hydrate only what is needed
↓
Hydrate other sections later
Wyraża się to za pomocą tych samych bloków @defer, rozszerzonych o mechanizmy inicjacji hydratacji. W poniższym przykładzie serwer nadal renderuje HTML wykresu, klient hydratuje go, gdy przeglądarka jest nieaktywna, a przy renderowaniu wyłącznie po stronie klienta blok ładuje się w momencie interakcji:
@defer (on interaction; hydrate on idle) {
<app-heavy-chart />
}
Zaletą takiej architektury jest to, że renderowanie HTML i nadawanie mu interaktywności przestają być jednym, całościowym krokiem. Konieczne jest włączenie progresywnej hydratacji w konfiguracji; sprawdź przewodnik SSR dla swojej wersji Angulara, aby poznać dokładną opcję dostawcy.
3. Anuluj przestarzałe żądania za pomocą resource()
Funkcja wyszukiwania w trakcie pisania to klasyczny przykład marnotrawstwa zasobów sieciowych. W miarę pisania przez użytkownika dane przechodzą przez wszystkie pośrednie wartości:
a
an
ang
angu
angul
angula
angular
Naiwna implementacja może wysyłać żądanie dla każdej z nich:
Request 1 → "a"
Request 2 → "an"
Request 3 → "ang"
Request 4 → "angu"
Request 5 → "angul"
Request 6 → "angular"
Tylko ostatnia odpowiedź ma znaczenie, więc pozostałe pięć to strata czasu. Nowoczesny Angular oferuje funkcje resource() i httpResource() do reaktywnego, asynchronicznego ładowania. Zaczynamy od sygnału przechowującego zapytanie:
const query = signal('');
Następnie deklarujemy zasób, którego funkcja params odczytuje ten sygnał, a którego loader wykonywać będzie żądanie. W poniższym przykładzie loader znajduje się na tej samej linii co zamknięcie nawiasu z params; są to oddzielne właściwości obiektu opcji:
const results = resource({
params: () => ({
q: query()
}), loader: ({ params, abortSignal }) =>
fetch(
`/api/search?q=${params.q}`,
{ signal: abortSignal }
).then(res => res.json())
});
Gdy tylko parametry ulegną zmianie podczas trwania ładowania, Angular anuluje poprzednie żądanie. Działa to tylko dlatego, że loader przekazuje dostarczony sygnał do funkcji fetch, na co należy zwrócić uwagę:
{ signal: abortSignal }
Jeśli tego pominąć, stara prośba będzie nadal wykonywana do końca. W rzeczywistym kodzie warto wprowadzić dwie poprawki: otoczyć zapytanie funkcją encodeURIComponent przed wstawieniem go do URL, a także pamiętać, że anulowanie nie jest równoznaczne z debouncingiem. Każde naciśnięcie klawisza nadal uruchamia prośbę, która jest następnie przerwana, więc jeśli chcesz uniknąć ich wysyłania, połącz zasób z sygnałem zapytania opartym na debouncingu. Naj szybszą prośbą jest ta, którą zatrzymujesz natychmiast po tym, jak staje się nieistotna, a ten podejście jest korzystne przy wyszukiwaniu, autodopasowywaniu, filtrowaniu, żywych panelach kontrolnych, szybko zmieniających się identyfikatorach oraz łańcuchach powiązanych wywołań.
4. Utrzymuj sygnały małe i skoncentrowane
Sygnały są wydajne, ale nadal można je używać w sposób, który niszczy tę wydajność. Częstym wzorcem jest używanie jednego sygnału do przechowywania stanu całej strony:
const pageState = signal({
user: {},
filters: {},
notifications: [],
products: [],
statistics: {},
settings: {}
});
Jeśli zmieniają się tylko filtry, powiadamiany jest każdy konsument pageState, ponieważ cała wartość sygnału została zastąpiona. Podział niezależnych elementów stanu na osobne sygnały zmniejsza zasięg wpływu. Pierwszy z nich wygląda następująco:
const user = signal(...);
a pozostałe podążają za tym samym wzorem (tutaj pokazane razem na jednej linii; w rzeczywistym kodzie każdy z nich znajduje się na osobnej linii):
const filters = signal(...);const notifications = signal(...);const products = signal(...);const statistics = signal(...);const settings = signal(...);
Teraz każdy konsument zależy tylko od stanu, który faktycznie odczytuje. Arkusz kalkulacyjny stanowi dobry model mentalny. Gdy zmienia się pojedyncza komórka:
A1 changes
nie chce się przeliczać całego arkusza, tylko łańcucha zależnego od niej:
A1
↓
Dependent cells
↓
Only affected UI
Sygnał dużego obiektu unieważnia wszystkich konsumentów, którzy interesują się tylko jego fragmentem; małe sygnały zapewniają węższy zakres unieważnienia. Zamiast zastanawiać się, czy cały stan zmieści się w jednym sygnale, zapytaj się, co powinno faktycznie zareagować, gdy zmieni się określona wartość. Jeśli naprawdę potrzebujesz złożonego obiektu, utwórz go za pomocą computed() na podstawie małych sygnałów, zamiast przechowywać go jako źródło prawdy.
5. Przenoś kosztowne obliczenia poza szablony
Wywoływanie funkcji z szablonu to jeden z najprostszych sposobów na powtarzanie tych samych operacji:
<div>
{{ calculateTotal(items()) }}
</div>
Wygląda to nieszkodliwie, ale Angular ponownie ewaluuje tę wyrażenie za każdym razem, gdy sprawdzana jest widok, więc kosztowna funkcja calculateTotal() jest wykonywana raz po raz, nawet gdy items się nie zmieniło. Rozwiązaniem jest sygnał computed(). Zadeklaruj źródłowy stan:
const items = signal<Item[]>([]);
Wyprowadź z tego wartość całkowitą:
const total = computed(() =>
items().reduce(
(sum, item) => sum + item.price,
0
)
);
i odczytaj wyprowadzony sygnał w szablonie:
<div>
{{ total() }}
</div>
computed() przechowuje w pamięci swój wynik i przelicza go ponownie tylko wtedy, gdy zmienił się sygnał, który odczytał, a nie przy każdej ewaluacji szablonu. Dzięki temu nadaje się idealnie do filtrowania, sortowania, agregacji, statystyk, wykresów na panelu kontrolnym oraz dowolnego innego wyprowadzonego stanu interfejsu. Lista produktów po filtracji ma taką samą strukturę, wychodząc od sygnału z terminem wyszukiwania:
const searchTerm = signal('');
i lista wyprowadzona, która zależy zarówno od produktów, jak i od terminu:
const filteredProducts = computed(() =>
products().filter(product =>
product.name.includes(searchTerm())
)
);
To zastępuje wywołania szablonów w ten sposób, przenosząc obliczenia do grafu reaktywnego:
{{ filterProducts(products(), searchTerm()) }}
Jeden szczegół: funkcja includes jest wrażliwa na wielkość liter, więc rzeczywiste wyszukiwanie zazwyczaj najpierw normalizuje obie strony. Traktuj computed() jako pamięć cache dla stanu pochodnego, który prawidłowo unieważnia się sam.
6. Ładowanie w tle ciężkich usług, a nie tylko komponentów
Ładowanie w tle zwykle omawia się na dwóch poziomach:
Lazy route
Lazy component
Usługi mogą jednak mieć własne duże drzewa zależności. Dobrym przykładem jest usługa do wstępnego przeglądania plików Markdown:
MarkdownService
│
├── marked
├── highlight.js
├── syntax definitions
└── other dependencies
Jeśli usługa jest używana tylko wtedy, gdy ktoś kliknie przycisk „Wstępny przegląd Markdown”, nie ma powodu, by pakiet początkowy zawierał parser, podświetlacz składni oraz ich reguły gramatyczne. Technika polega na załadowaniu usługi poprzez dynamiczne importowanie w momencie, gdy jest ona potrzebna. Koncepcyjnie wygląda to tak, przy użyciu pomocnika injectAsync():
const markdownService = injectAsync(
() =>
import('../markdown.service')
.then(m => m.MarkdownService)
);
Komponent następnie czeka na usługę wewnątrz obsługi, która jej potrzebuje:
async preview(content: string) {
const service = await this.markdownService();
i używa jej do renderowania treści:
this.html = service.render(content);
}
Traktuj injectAsync() jako wzorzec, a nie gwarantowaną podstawową API: sprawdź, czy twoja wersja Angular lub używana biblioteka ją dostarczają. Ten sam efekt można osiągnąć za pomocą zwykłego await import('../markdown.service') wewnątrz obsługi, a następnie rozwiązania klasy poprzez injector. W obu przypadkach usługa oraz jej ciężkie zależności trafiają do osobnego chunka. Ogólną zasadą jest ładowanie kosztownych zależności tam, gdzie są one przydatne, a nie tylko na granicach tras.
7. Traktuj odczyty i zapisy w DOM jako kosztowną API
Czasami wąskim gardłem nie jest wcale JavaScript, lecz silnik układu przeglądarki. Dzieje się tak, gdy kod łączy pomiędzy sobą pomiary a modyfikacje:
read DOM
write DOM
read DOM
write DOM
read DOM
write DOM
Każde odczytanie po zapisie może zmusić przeglądarkę do synchronicznego ponownego obliczenia układu, co jest zjawiskiem znanym jako layout thrashing. Angular oferuje do tego hooki uwzględniające proces renderowania. Pierwszy z nich to:
afterNextRender()
a drugi, który obsługuje wyraźne fazy, to:
afterRenderEffect()
Używając afterRenderEffect(), umieszcza się pomiary w fazie earlyRead:
afterRenderEffect({
earlyRead: () => {
return element.getBoundingClientRect();
},
a mutacje w fazie write, która otrzymuje wartość zwróconą przez odczyt (ten fragment to reszta tej samej wywołania w TypeScript, a nie CSS):
write: height => {
element.style.height = `${height}px`;
}
});
Angular wykonywa wszystkie fazy odczytu komponentów przed ich fazami zapisu, dzięki czemu przeglądarka widzi prace zgrupowane w ten sposób:
READ
↓
READ
↓
READ
↓
WRITE
↓
WRITE
↓
WRITE
zamiast ciągle się naprzemiennie zmieniać:
READ → WRITE → READ → WRITE
Ta koordynacja oparta na fazach ma największe znaczenie w przypadku wykresów, map, animacji, obsługi zmiany rozmiaru i przewijania, bibliotek interfejsu użytkownika od firm trzecich oraz złożonych paneli kontrolnych, gdzie wiele komponentów modyfikuje DOM po każdym renderowaniu.
8. Wykrywanie zmian partiami z łączeniem zdarzeń i wykonywań
W aplikacjach opartych na strefach kilka wyzwalaczy może zostać uruchomionych szybko po sobie w ramach jednej logicznej akcji użytkownika:
click
↓
event bubbles
↓
parent handler
↓
ngZone.run()
↓
another update
Bez łączenia każdy krok może planować własną rundę wykrywania zmian. Angular może je połączyć, gdy włączysz obie opcje w provideZoneChangeDetection:
bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection({
eventCoalescing: true,
runCoalescing: true
})
]
});
Zamiast jednej rundy na każde zdarzenie:
Event A → Change Detection
Event B → Change Detection
Event C → Change Detection
Przekrywające się zadania sprowadzają się do jednej rundy:
Event A ─┐
Event B ─┼──→ One Change Detection
Event C ─┘
Jest to szczególnie przydatne w aplikacjach z wieloma obsługującymi wydarzenia funkcjami, aktywnymi strumieniami WebSocket, częstymi wywołaniami ngZone.run() lub bardzo interaktywnymi panelami sterowania. Należy pamiętać, że te opcje odnoszą się do detekcji zmian opartej na Zone.js; nowsze wersje Angular preferują detekcję zmian bez użycia Zone, gdzie planowanie jest już kierowane przez sygnały i wyraźne powiadomienia, dlatego sprawdź, który tryb używa twoja aplikacja, zanim się ich posłużysz.
9. Znajdź zależności, których nie chciałeś włączyć
Część największych problemów z wydajnością nie wynika z samego Angular, lecz z problemów z importami. Możesz potrzebować tylko jednej funkcji z biblioteki, ale twoja struktura importów może ściągnąć znacznie więcej:
Application
│
└── Library
│
├── Feature A
├── Feature B
├── Feature C
├── Feature D
└── Feature E
podczas gdy w rzeczywistości użyto tylko:
Feature A
Dlatego analiza pliku bundle powinna być częścią każdego badania wydajności. Utwórz statystyki budowy wersji produkcyjnej:
ng build --configuration=production --stats-json
Następnie sprawdź wyniki, aby zobaczyć, które pliki i biblioteki dominują w treści przesyłanej. Narzędzia takie jak source-map-explorer działają na podstawie map źródłowych, więc włącz je w procesie budowania, który analizujesz; format pliku ze statystykami zależy od narzędzia budującego używanego w projekcie, więc sprawdź, jakie analizatory ono obsługuje. Ważne nie jest to, czy aplikacja wydaje się wolna, ale dokładnie to, co wysyłasz do przeglądarki. Gdy to stanie się widoczne, większość optymalizacji staje się oczywista. Aby uniknąć regresji później, rozważ wdrażanie budżetów wydajnościowych w CI.
10. Przeniesienie obciążonych obliczeń z głównej wątku
Czasami problemem nie jest framework, lecz algorytm. Załóżmy, że panel analityczny otrzymuje zbiór danych tej wielkości:
100,000 records
i wykonywa w przeglądarce statystyki, przetwarzanie obrazów, operacje geometryczne, dużą ilość transformacji, złożone analizy lub agregacje. Na głównej wątku ta praca konkuruje ze wszystkim innym:
Main Thread
│
├── Angular
├── Rendering
├── User input
├── Animations
└── 100K-record calculation
Rezultatem jest niska wydajność: zamarłe elementy wejściowe, przerywane animacje oraz opóźniona renderizacja. Web Worker przenosi obliczenia na oddzielny wątek i zwraca tylko wynik:
Main Thread Worker
│ │
│──── large dataset ─────>│
│ │
│ Calculate
│ Transform
│ Analyze
│ │
│<──── result ─────────────│
│
Render UI
Angular CLI może stworzyć takiego pracownika oraz skonfigurować jego ustawienia:
ng generate web-worker app
a komponent tworzy go przy użyciu adresu URL względem modułu, aby narzędzie do pakowania mogło go znaleźć i skompilować:
const worker =
new Worker(
new URL('./app.worker', import.meta.url)
);
Pracownik nie sprawia, że praca samego Angulara staje się tańsza. Jego zadaniem jest zapobieganie temu, by kosztowna logika aplikacji blokowała renderowanie i interakcje użytkownika. Używaj pracowników tylko wtedy, gdy obliczenia są rzeczywiście intensywne, ponieważ dane przekazywane między wątkami są kopiowane za pomocą strukturalnego klonowania, a ten proces również ma swój własny koszt. Aby uzyskać neutralne pod względem frameworka spojrzenie na tę decyzję, zapoznaj się z kiedy przenieść obliczenia CPU do Web Workera.
Łączenie tych technik na jednej liście kontrolnej
To nie są dziesięć oddzielnych trików; one wzajemnie się uzupełniają. Nieoptymalizowana lista kontrolna analityczna wykonywa wszystko od razu:
Browser
│
├── Download huge JS bundle
├── Initialize everything
├── Hydrate everything
├── Start every API request
├── Recalculate large state
├── Execute expensive template functions
├── Perform DOM reads/writes
└── Calculate large datasets on main thread
Dzięki zastosowaniu tych technik każda warstwa wykonuje tylko to, co wymaga dany moment:
Browser
│
├── Smaller initial bundle
│ └── @defer
│
├── Partial interactivity
│ └── Incremental hydration
│
├── Only latest API request
│ └── resource/httpResource
│
├── Narrow reactive updates
│ └── Fine-grained signals
│
├── Cached derived values
│ └── computed()
│
├── Heavy services loaded later
│ └── injectAsync()
│
├── Coordinated DOM work
│ └── afterRenderEffect()
│
├── Fewer redundant CD cycles
│ └── Event coalescing
│
├── Smaller dependencies
│ └── Bundle analysis
│
└── Heavy computation elsewhere
└── Web Worker
Aplikacja nie działa szybciej, lecz wykona o wiele mniej operacji.
Kompaktowa formuła wydajności
Jednym ze sposobów podsumowania tego podejścia jest przedstawienie go jako sumy redukcji:
Performance
=
Less JavaScript
+
Less Network Work
+
Less Reactive Work
+
Less DOM Work
+
Less Main-Thread Work
Mówiąc jeszcze krócej: szybki Angular oznacza wykonywanie mniej operacji, robienie tego później i tylko wtedy, gdy jest to konieczne.
Praktyczna kolejność optymalizacji
Nie stosuj wszystkiego naraz. Najpierw zmierz wyniki, a następnie przystąp do optymalizacji w przybliżonej tej kolejności, ponieważ wcześniejsze kroki mają zazwyczaj największy wpływ na czas uruchamiania.
Najpierw zmniejsz początkowy kod JavaScript:
@defer
injectAsync()
bundle analysis
Następnie zmniejsz początkowe zadania związane z hydratacją:
Incremental Hydration
Trzecio, ogranicz niepotrzebne operacje sieciowe:
resource()
httpResource()
Czwartym krokiem jest ograniczenie niepotrzebnej walidacji reaktywnej:
Fine-grained signals
computed()
Piątym krokiem jest zmniejszenie obciążenia związанego z renderowaniem:
afterRenderEffect()
afterNextRender()
Szóste: usuń zbędne wykrywanie zmian:
eventCoalescing
runCoalescing
Siedemte: wyeliminuj wąskie gardła procesora:
Web Workers
Mierz przed optymalizacją
Najważniejszą zasadą jest unikanie ślepej optymalizacji. Rozsypywanie jej wszędzie nie stanowi strategii:
OnPush
Podobnie jak wprowadzanie czegokolwiek z tego bez wiedzy o przyczynie wąskiego gardła:
memoization
workers
RxJS operators
defer blocks
Przeprowadź profilowanie za pomocą Chrome DevTools, Lighthouse, Angular DevTools, analizy pliku produkcyjnego oraz panelu wydajności przeglądarki. Następnie zadaj podstawowe pytanie:
What is actually slow?
I zawęż odpowiedź do konkretnej kategorii:
Network?
JavaScript?
Change detection?
Rendering?
Layout?
CPU?
Tylko wtedy, gdy dowiesz się, która z tych kwestii dominuje, powinieneś wybrać odpowiednią technikę. Po każdej zmianie mierz ponownie, aby upewnić się, że poprawa jest rzeczywista.
Starsze porady dotyczące wydajności Angulara koncentrowały się głównie wokół jednego pytania:
"How can I make change detection faster?"
Współczesny Angular zaleca bardziej przydatne pytanie:
"Why am I running this work at all?"
To drugie pytanie ma zastosowanie we wszystkich warstwach. Dlaczego ściągać ten komponent teraz? Dlaczego hydratować tę sekcję teraz? Dlaczego czekać, aż zakończy się żądanie HTTP, skoro użytkownik już zmienił parametry zapytania? Dlaczego unieważniać cały obiekt stanu z powodu jednego pola? Dlaczego przeliczać wartość, której dane wejściowe się nie zmieniły? Dlaczego modyfikować DOM przed zakończeniem renderowania? Dlaczego wykonywać intensywne obliczenia na wątku UI? Każda niepotrzebna operacja to szansa.
Główne wnioski
Dziesięć tych technik dotyczy różnych warstw i każda z nich eliminuje określony rodzaj marnotrawstwa:
@deferw połączeniu z bezpośrednimi importami zmniejsza początkową ilość kodu JavaScript.- Hydryacja stopniowa zmniejsza początkowe obciążenie związane z hydratowaniem.
resource() z sygnałem abort usuwa przestarzałe operacje sieciowe.computed() zapobiega powtarzającym się obliczeniom.afterRenderEffect() zapobiegają problemom z układem elementów.Naj szybsza aplikacja Angular to nie ta, która naj szybciej wykonywa JavaScript; to ta, która wykonywa jak najmniej niepotrzebnego JavaScript. Cała strategia sprowadza się do jednego schematu:
MAKE ANGULAR FAST
│
┌──────────┴──────────┐
│ │
DO LESS DO LATER
│ │
┌─────┼─────┐ ┌─────┼─────┐
│ │ │ │ │ │
Less Less Less Defer Lazy Hydrate
JS CPU DOM code data later
│ │ │ │ │ │
└─────┴─────┴─────────┴─────┴─────┘
│
▼
FASTER EXPERIENCE
Pomierz, znajdź marnotrawioną pracę, usuń ją i pomierz ponownie.
Literatura pokrewna
- Nieblokujące rozpoczynanie pracy Angulara: Ładowanie konfiguracji za pomocą rxResource — Dowiedz się, dlaczego blokujący APP_INITIALIZER pozostawia użytkowników Angulara na pustej stronie, oraz jak rxResource, narzędzie do obsługi szkieletu aplikacji i rozwiązywania problemów z trasami, umożliwia ładowanie konfiguracji bez jego użycia.
- Zamiana flag ładowania w Angularu za pomocą dyskryminowanej unii stanów asynchronicznych — Modeluj operacje asynchroniczne w Angularu jako jedną zunion typów, wyznaczaj wartości logiczne za pomocą sygnałów obliczeniowych i steruj każdą zmianą stanu za pomocą pojedynczego operatora RxJS.