Wróć do strony głównej
Angular

Jak używać bloku defer w Angularze, aby zwiększyć wydajność?

Zapewnienie użytkownikom satysfakcjonujących doświadczeń jest kluczowe – zwłaszcza, gdy oczekują oni na jakieś zdarzenie. W takich przypadkach pomocne może być wykorzystanie placeholdera czy loading indicatora. Co jednak w sytuacji, kiedy nasz komponent jest duży lub użytkownik posiada słabe połączenie internetowe? Możemy to poprawić kontrolując wstępne pobieranie zasobów (ang. prefetching). Sprawdźmy to.

Czym jest blok defer w Angularze?

Deferrable views w Angularze dostarczają przydatną funkcję, znaną jako blok defer. Pozwala ona na kontrolowane opóźnienie ładowania wybranych elementów czy komponentów, dopóki nie będą one konieczne. Blok defer jest kontenerem dla odroczonej zawartości i oferuje wiele wyzwalaczy do kontrolowania czasu i sposobu jej ładowania.

Identyfikacja problemów w konfiguracji ścieżek 

Wyobraźmy sobie następujący scenariusz. Jesteśmy w trakcie tworzenia dużej aplikacji z wieloma funkcjonalnościami i widokami.

export const routes: Routes = [
   {
      path: ‘yellow’,
      component: YellowComponent
   },
   {
      path: ‘cyan’,
      component: CyanComponent
   },
   {
      path: ‘purple’,
      component: PurpleComponent
   }
]

Każda z tych funkcjonalności posiada dużą i złożoną konfigurację ścieżek. Angular domyślnie ładuje je wszystkie i wtedy… pojawia się problem z wydajnością.  

Powyższa konfiguracja ścieżek powoduje problemy związane z Core Web Vitals. Angular rozpoznaje te trzy funkcjonalności i wczytuje je podczas kompilacji, co skutkuje umieszczeniem ich wszystkich w pliku main.js. Na diagramie poniżej funkcjonalności reprezentowane są przez kolory: żółty, cyjan oraz fioletowy.  Chociaż aplikacja będzie działać poprawnie to zwiększenie rozmiaru początkowego bundle’a wpłynie negatywnie na Core Web Vitals i wydajność aplikacji.

Poprawa wydajności w Angularze poprzez podział kodu i lazy-loading

Niektóre problemy związane z naruszeniem metryk LCP i TTFB  można rozwiązać poprzez podział kodu. Angular oferuje API, które mogą w tym pomóc, takie jak użycie funkcji loadComponent przy konfiguracji ścieżek.

export const routes: Routes = [
   {
      path: ‘yellow’,
      loadComponent: () => 
         import(‘./yellow.component’).then(it => it.YellowComponent)
   },
   {
      path: ‘cyan’,
      loadComponent: () =>
         import(‘./cyan.component’).then(it => it.CyanComponent)
   },
   {
      path: ‘purple’,
      loadComponent: () => 
      import(‘./purple.component’).then(it => it.PurpleComponent)
   }
]

Dzięki podaniu ścieżki i zwracaniu Promise’a Angular może wykorzystać lazy-loading do załadowania potrzebnych komponentów bez powodowania problemów z wydajnością. Dzieje się tak dzięki tworzeniu oddzielnych chunków.

Pozwala to na zmniejszenie rozmiaru początkowego bundle’a i ogólne zwiększenie wydajności.

Podział funkcjonalności

Wyobraźmy sobie sytuację, w której mamy stronę lub funkcjonalność, która zawiera wiele komponentów. Każda z tych funkcjonalności może działać inaczej, a każdy użytkownik może przejść przez nią na swój własny, unikalny sposób. Oznacza to, że podział dużej funkcjonalności na mniejsze, lazy-loadowane fragmenty może być przydatny do optymalizacji doświadczeń użytkownika, uwzględniając różne sposoby korzystania z aplikacji. Przykładowo, jeśli Użytkownik A korzystając ze Ścieżki A potrzebuje chunku cyan-1, a Użytkownik B korzystając ze Ścieżki B potrzebuje chunku cyan-2 możemy zagwarantować, że każdy użytkownik ładuje tylko te komponenty, które są niezbędne dla jego konkretnej ścieżki, dzięki czemu poprawimy wydajność i skrócimy niepotrzebne ładowanie.

Aby wygenerować tego rodzaju chunki możemy stworzyć instancję komponentu:

const componentInstance = await import(‘./cyan.component’)
   .then(it => it.CyanComponent);

a następnie przekazać ją jako argument metody createComponent klasy ViewContainerRef:

this.vcr.createComponent(componentInstance);

Takie rozwiązanie jednak ma kilka wad. Dla dużych komponentów lub słabych połączeń internetowych powszechną praktyką jest wykorzystanie loading indicatora, aby użytkownik był świadomy, że coś się ładuje. Ważna jest również obsługa błędów, które mogą wystąpić w trakcie ładowania. Ponadto musimy zwrócić uwagę na ręczne sprzątanie, aby nie tworzyć wielu instancji komponentu. Dodatkowo, nie powinniśmy dodawać komponentu do tablicy imports, ponieważ spowoduje to jego natychmiastowe załadowanie, zamiast wykorzystania lazy-loadingu.

Deferred Loading i tworzenie chunków

W celu rozwiązania tych problemów, możemy wykorzystać blok defer. Pozwoli nam to na opóźnienie ładowania określonych części dużych komponentów, aż do momentu, kiedy staną się one niezbędne. Korzystając z bloku defer, możemy poprawić doświadczenia użytkownika i zagwarantować, że aplikacja działa jak najbardziej efektywnie.

W naszym przypadku konfiguracji trzech lazy-loadowanych komponentów (yellow, cyan i purple) umieścimy w komponencie CyanComponent blok defer, aby wykorzystać lazy-loading w komponencie.

`cyan.component.html`

@defer {
   <div>
<!-- Część komponentu wykorzystująca lazy-loading - ->
(...)
</div>
}
<div>
	<!-- Część komponentu ładowana natychmiastowo - ->
	(...)
</div>

Do drzewa komponentu został dodany blok defer, który jest identyfikowany przez Angular podczas kompilacji. W rezultacie generowane są następujące chunki: yellow, purple, chunk-cyan-1, chunk-cyan-2.

Dodatkowo na podstawie wyrażenia logicznego możemy kontrolować, kiedy pobrać i wyświetlić komponent:

@defer (when isVisible) {
   <app-cyan />
}

Kiedy wartość isVisible zmieni się na true, Angular pobierze i wyświetli zawartość  komponentu CyanComponent. Jednak, gdy wartość isVisible zmieni się ponownie na false, zawartość komponentu CyanComponent nie zostanie przywrócona do stanu początkowego, jest to więc operacja jednorazowa.

Poprawa doświadczeń użytkownika za pomocą placeholderów

Wsparcie użytkownika za pomocą placeholderów może bardzo korzystnie wpłynąć na jego doświadczenia. Na przykład, posiadanie w formularzu rejestracji placeholderów dla pól, takich jak imię czy nazwisko może pomóc użytkownikom zrozumieć, jakie informacje powinni podać. Podobnie, w przypadku wykorzystania bloku defer w aplikacji Angularowej, użycie placeholdera może pomóc użytkownikowi zrozumieć, co dzieje się w aplikacji oraz pozwoli uniknąć ewentualnej dezorientacji, ponieważ placeholder jest widoczny do momentu wyrenderowania komponentu.

@defer (when isVisible) { <app-cyan /> } @placeholder { <span> Zostanie nadpisany zawartością komponentu cyan </span> }

Ważne jest również, aby pamiętać, że unikanie migotania aplikacji może dodatkowo poprawić wrażenia użytkownika. Zapewnienie kontroli nad placeholderem, takie jak ustawienie minimalnego czasu jego widoczności może być w tym bardzo pomocne. Takie rozwiązanie zapewni płynność w działaniu, a aplikacja czy strona internetowa będzie wydawać się bardziej profesjonalna i dopracowana. 

@defer (when isVisible) {
   <app-cyan />
} @placeholder(minimum 500ms) {
  <span> Zostanie nadpisany zawartością komponentu cyan </span>
}

Wykorzystując blok defer opóźniamy ładowanie komponentu, dlatego powinniśmy dodać loading indicator, aby poinformować użytkownika, że coś się dzieje. Zawartość wewnątrz bloku @loading jest widoczna podczas pobierania fragmentu wykorzystującego lazy-loading. Aby dodatkowo poprawić wrażenia użytkownika możemy wykorzystać dodatkowe parametry, które pozwalają nam zdefiniować czas, po którym zostanie wyświetlona zawartość oraz minimalny czas przez jaki nasz loading indicator będzie widoczny.

@defer (when isVisible) {
   <app-cyan />
} @placeholder(minimum 500ms) {
  <span> Zostanie nadpisany zawartością komponentu cyan </span>
} @loading(after 100ms; minimum 1s) {
  <span> Ładowanie... </span>
}

Jeśli coś pójdzie nie tak i podczas ładowania wystąpi błąd, możemy wyświetlić użytkownikowi komunikat, wykorzystując do tego blok @error.

@defer (when isVisible) {
   <app-cyan />
} @placeholder(minimum 500ms) {
  <span> Zostanie nadpisany zawartością komponentu cyan </span>
} @loading(after 100ms; minimum 1s) {
  <span> Ładowanie... </span>
} @error {
  <span> Ups! Pobieranie nie powiodło się </span>
}

Różne rodzaje wyzwalaczy w Angularze

Angular to potężny framework, które oferuje różnorodne rodzaje interakcji, umożliwiając tworzenie wrażeń przyciągających uwagę użytkownika. Zrozumienie, jak działają te wyzwalacze jest kluczowe dla programistów, którzy chcą tworzyć solidne i funkcjonalne aplikacje w oparciu o Angulara. Każdy z tych wyzwalaczy posiada unikalne cechy i funkcjonalności, które można wykorzystać do wzbogacenia doświadczeń użytkownika oraz poprawy funkcjonalności w aplikacji.

Podczas, gdy słowo kluczowe `when` określa warunek logiczny, słowo kluczowe `on` pozwala na rozpoczęcie ładowania przy wykorzystaniu predefiniowanych wyzwalaczy:

idle rozpoczyna ładowanie, gdy przeglądarka osiągnie stan bezczynności (ustalany przy użyciu metody metody requestIdleCallback), a więc w przypadku gdy przeglądarka nie ma aktualnie żadnego zadania wymagającego dużego nakładu pracy. Jest to zachowanie domyślne.

immediate ładuje chunk natychmiast po wyrenderowaniu strony.

interaction wyzwalany jest po interakcji użytkownika poprzez zdarzenie click lub keydown.

hover wyzwalany jest po najechaniu kursorem na element, wykorzystuje przeglądarkowe zdarzenia mouseenter oraz focusin.

viewport wyzwalany jest, gdy element jest widoczny w widoku użytkownika – Angular wykorzystuje do tego Intersection Observer API.

timer pobiera i wyświetla element po upływie określonego czasu.

W jednej instrukcji można zdefiniować wiele wyzwalaczy jednocześnie (również wykorzystując razem słowa kluczowe `on` oraz `when`). Do obsługi takich instrukcji Angular wykorzystuje operator logiczny OR.

@defer (on viewport; when condition) {
   <app-cyan />
} @placeholder {
   <span> Zostanie nadpisany zawartością komponentu cyan </span>
}

Rozróżnianie niejawnych i jawnych wyzwalaczy

Dla wyzwalaczy interaction, hover oraz viewport możemy wyróżnić jawną i niejawną definicję wyzwalacza. Biorąc jako przykład wyzwalacz interaction:

Niejawny wyzwalacz interaction ładuje blok po kliknięciu użytkownika w placeholder, ponieważ jest to jedyny widoczny dla użytkownika element:

@defer (on interaction) {
   <app-cyan />
} @placeholder {
   <span> Kliknij tutaj, aby załadować </span>
}

Natomiast jawna deklaracja wyzwalacza interaction definiuje konkretny element poprzez przekazanie referencji do jego szablonu. Element ten powinien być kliknięty, aby załadować komponent wewnątrz bloku defer. Warto zaznaczyć, że nie ma potrzeby deklarowania obsługi zdarzenia click.

<button #trigger> Click to load </button>

@defer (on interaction(trigger)) {
   <app-cyan />
}

Wstępne ładowanie komponentów

Jeśli komponent jest bardzo ciężki lub użytkownik ma wolne połączenie, możemy potrzebować jeszcze większej kontroli nad procesem odroczonego ładowania zawartości. W celu rozwiązania tego problemu, Angular oferuje deklaratywne podejście do wstępnego pobierania chunków za pomocą słowa kluczowego prefetch. Programiści mogą wstępnie załadować dane, aby poprawić doświadczenia użytkownika, korzystając z dowolnej kombinacji dotychczas opisanych wyzwalaczy.

@defer (on “action”; prefetch on “action”) {
   <app-cyan />
}

@defer (on “action”; prefetch when “boolean expr”) {
   <app-cyan />
}

@defer (when “boolean expr”; prefetch on “action”) {
   <app-cyan />
}

@defer (when “boolean expr”; prefetch when “boolean expr”) {
   <app-cyan />
}

Postępy w Angularze: usprawnienia w doświadczeniach użytkownika i wydajności ładowania

Angular to stale rozwijający się framework, który ciągle poprawia doświadczenia użytkownika i optymalizuje wydajność ładowania. Pomimo braku bezpośredniej opcji do wyboru konkretnych urządzeń wyświetlających aplikację, zespół Angulara nieustannie pracuje, aby dostarczyć programistom lepsze narzędzia i możliwości.  

Dzięki połączeniu bloku defer oraz wstępnego ładowania programiści mogą zoptymalizować wydajność swoich aplikacji oraz zapewnić płynne i przyjemne doświadczenia użytkownika w oparciu o określone wymagania. Framework ten dostarcza programistom wszystko, czego potrzebują do tworzenia nowoczesnych aplikacji, a także inspiruje ich do przekraczania granic tego, co jest możliwe.

Współtwórca:
Miłosz Rutkowski
Damian Maduzia

Tłumaczenie:
Sebastian Smulski

O autorze

Fanis Prodromou

Jestem full-stack web developerem z pasją do Angulara i NodeJs. Mieszkam w Atenach-Grecji i pracowałem w wielu dużych firmach. Podczas moich 14 lat kodowania zdobyłem ogromne doświadczenie w zakresie jakości kodu, architektury aplikacji oraz ich wydajności.

Zdając sobie sprawę z tego, jak szybko rozwija się informatyka i aspekty techniczne, staram się być na bieżąco, uczestnicząc w konferencjach i meetupach, studiując i próbując nowych technologii. Uwielbiam dzielić się swoją wiedzą i pomagać innym programistom.

„Sharing is Caring”

Uczę Angulara w firmach korporacyjnych poprzez instytut Code.Hub, piszę artykuły i tworzę filmy na YouTube.

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.

Dodaj komentarz

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