Wróć do strony głównej
Angular

Zwiększ wydajność swojej aplikacji z NgOptimizedImage

Wprowadzenie

Jak mówi pewne powiedzenie, zdjęcie jest warte tysiąc słów. W świecie webowym, natomiast, obraz może ważyć więcej niż wszystkie słowa na Twojej stronie razem wzięte. Aby sprawić, by przeglądanie naszych użytkowników było bardziej przyjemne, musimy zapewnić możliwie największą wydajność w naszych aplikacjach. W tym artykule przyjrzymy się optymalizacji dostarczania zdjęć za pomocą NgOptimizedImage.

Przykład aplikacji

Stworzyłem bardzo prostą aplikację Angularową, która wykorzystuje JSONPlaceholder API, aby wyświetlić dużo zdjęć w galerii. Samo działanie aplikacji jest bardzo proste: otrzymuje listę obrazków, które następnie wyświetla, co widać na przykładzie poniżej:

Sama galeria działa, ale wymaga trochę czasu na załadowanie. Nie jest to idealna sytuacja – jak wszyscy wiemy, użytkownicy są niecierpliwi. Pora to naprawić! Aby zacząć, stworzymy kompilację produkcyjną, uruchomimy ją lokalnie, i użyjemy Chrome Lighthouse, aby zanalizować ładowanie strony.

Moje testy otrzymały następujące wyniki:

  • First Contentful Paint (FCP): 2.3 sekundy. Jest to czas pomiędzy momentem wejścia na naszą stronę przez użytkownika a momentem, w którym cokolwiek zobaczył na ekranie. Z reguły czas ten nie powinien przekraczać 1.8 sekundy.
  • Largest Contentful Paint (LCP): 3.3 sekundy. Ten pomiar reprezentuje czas, który strona potrzebuje do wyrenderowania swojej głównej treści. Aby zapewnić dobre doświadczenie przeglądania, ten czas powinien trwać poniżej 2.5 sekundy.
  • First Input Delay (FID)/Total Blocking Time: 230 ms. Jest to czas, podczas którego nie możemy w żaden sposób wejść w interakcję ze stroną internetową.
  • Cumulative Layout Shift: 1.251. Pomiar ten mierzy wszystkie zmiany w układzie strony w trakcie jej cyklu życia. Powinniśmy celować w wynik równy 0.1 lub niższy.

Sprawdźmy teraz jak możemy zoptymalizować stronę.

Zakładka zasobów sieciowych w narzędziach developerskich przeglądarki pokazuje nam, że strona pobiera swoje obrazy dopiero po zakończeniu przetwarzania wszystkich swoich plików JavaScript, stylów i fontów. W niektórych przypadkach, takich jak obraz zaznaczony na zrzucie powyżej, przeglądarka czeka nawet 600ms zanim pobierze obraz. Później, pobiera resztę zasobów równolegle, pomimo że wiele z nich jest nie jest od razu widoczne dla użytkownika.

Możliwe rozwiązania

Podążając za Core Web Vitals i za zdrowym rozsądkiem developera, możemy podjąć się kilku kroków, aby usprawnić działanie strony. Tak jak już było to wspomniane wcześniej, wyniki, które powinniśmy osiągać to:

  • First Contentful Paint < 1.8 sekundy (2.3s)
  • Largest Contentful Paint < 2.5 sekundy (3.3s)
  • First Input Delay < 100ms (230ms)
  • Cumulative Layout Shifts < 0.1 (1.251)
  • Wynik Chrome Lighthouse > 85 (32)

Aby to zrobić, powinniśmy podjąć się kilku działań:

    • Optymalizacja obrazów. Przez to rozumiem priorytetyzację, który obraz zostanie pobrany najpierw, i żądany wcześniej, jeśli to możliwe.
    • Zwiększenie prędkość ładowania aplikacji. Do tego wynik pomiaru Largest Contentful Paint powinien wynosić poniżej 2.5 sekundy.
    • Biorąc pod uwagę specyfikację znacznika <img>, posiada on kilka atrybutów, które możemy wykorzystać, aby zoptymalizować ładowanie obrazów. Są to:
  • fetchpriority
  • loading
  • sizes
  • srcset

Implementacja lazy loadingu dla obrazów, które nie są początkowo widoczne dla użytkownika. Ogólnie, dla większości aplikacji obrazy powinny być ładowane wyłącznie tuż przed ich wyświetleniem – w ten sposób zwolnimy znaczną ilość przepustowości użytkownika.

Normalnie, gdy spotkamy się z problemem zbudowania podobnej galerii obrazów, pomyślelibyśmy o stworzeniu komponentu obrazka, dyrektywy lub kontenera i zbudowaniu całej logiki samodzielnie. Dla tego konkretnego zadania, natomiast, możemy po prostu użyć gotowego rozwiązania: dyrektywy NgOptimizedImage.

Dyrektywa NgOptimizedImage

Optymalizacja obrazów jest tak częstym działaniem, że zespół developerski Angulara stworzył gotowe rozwiązanie w formie dyrektywy NgOptimizedImage, dostępnej od wersji 13.4.0. Aby ją wykorzystać, możemy zaimportować ją z biblioteki @angular/common i dodać ją do tablicy importów w module naszego komponentu.

Żeby znacznik img zadziałał, musimy zmienić nasz atrybut src na ngSrc. Musimy także sprecyzować szerokość i wysokość – w innym przypadku otrzymamy błąd. Dzięki sprecyzowaniu wymiarów obrazów zapobiegniemy wszelkim ruchom w układzie strony związanych z ich ładowaniem.

Gdy już to zrobimy, otrzymamy następujący błąd:

Informuje on nas o tym, że wskazany obraz to element LCP, ale nie ma on zaznaczonego priorytetu. Naprawienie tego sprawia, że obraz będzie miał wyższy priorytet podczas ładowania, skreślając kolejną pozycję z naszej listy.

Po dodaniu flagi priority, znacznik img wygląda teraz jak na zrzucie powyżej. Niestety, nie jest to koniec naszych problemów. Musimy zmierzyć się z następnym ostrzeżeniem.

Podążając za treścią ostrzeżenia, musimy dodać nazwę domeny naszego serwera obrazów jako znacznik wstępnego połączenia w znaczniku <head> naszego dokumentu. Ponownie, pomaga to przyspieszyć ładowanie obrazów. W naszym przypadku używamy domeny zastępczej; Ty natomiast, musisz zamienić ją na domenę swojego serwera.

Po dodaniu domeny wstępnego połączenia, nasza konsola jest nareszcie wolna od błędów i ostrzeżeń. Możemy teraz zbadać stronę, żeby zobaczyć co Angular zrobił z naszymi obrazami.

Można zauważyć że znacznik img ma teraz kilka nowych atrybutów. Atrybut loading ma wartość eager (natychmiastowe) — możliwe wartości tutaj to lazy (opóźnione), które jest domyślną wartością, a także eager i auto. Ponadto, widoczny jest teraz atrybut fetchpriority o wartości high. Oznacza to, że obraz ma wysoki priorytet pobierania. Angular również wygenerował atrybut srcset dla naszego obrazu — dba on o to, żeby zdjęcia były zawsze żądane w odpowiednim rozmiarze.

Następny obraz jaki zbadamy również zawiera te atrybuty:

ak widać, jego ładowanie będzie opóźnione, a jego priorytet ładowania ma wartość auto. Sprawia to, że obraz ten jest mniej istotny, aby zmniejszyć jego wpływ na wydajność aplikacji.

W poniższym porównaniu widzimy dwie strony: jedną, używającą NgOptimizedImage, i jedną bez tej dyrektywy. Pierwszy zrzut ekranu pokazuje próby załadowania wszystkich obrazów przez przeglądarkę, co blokuje wykonanie skryptów. Drugi zrzut natomiast przedstawia bardziej zoptymalizowane podejście ładowania jedynie obrazów obecnie widocznych na ekranie.

Bez ngSrc

Z ngSrc i dyrektywą NgOptimizedImage

Póki co, zaimportowaliśmy jedynie dyrektywę NgOptimizedImage i zaimplementowaliśmy jej sugestie. Stwórzmy kompilację produkcyjną, i ponownie za pomocą Lighthouse sprawdźmy, czy udało nam się w jakikolwiek sposób poprawić wydajność naszej aplikacji.

Z powyższymi wynikami nie sposób się kłócić. W znaczny sposób poprawiliśmy wyniki wszystkich pomiarów, którym się przyjrzeliśmy. Jest to szczególnie niezwykłe, biorąc pod uwagę ilość pracy, która była potrzebna, żeby osiągnąć ten wynik. Możemy jednak pójść jeszcze dalej. NgOptimizedImage daje nam do dyspozycji więcej opcji, które możemy wykorzystać do dalszej kalibracji wydajności naszej aplikacji.

  • fill — ten atrybut pozwala nam pominąć wymiary obrazu, po prostu wypełniając element jego rodzica. Rodzic, natomiast, musi mieć ustawioną właściwość position na wartość relative, fixed, albo absolute. Właściwość object-fit obrazu może następnie być ustawiona na wartości contain albo cover.
  • ngSrcset — deskryptor gęstości obrazu oddzielony przecinkami.
  • sizes — ten atrybut może być użyty z obrazami responsywnymi. Domyślnie responsywne breakpointy mają wartości [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840]. ngSrcset i sizes są stworzone, aby działały razem. Jeśli atrybut ngSrcset jest obecny, srcset zostanie wygenerowany na podstawie sprecyzowanych wartości atrybutu sizes.
  • Image loaders — ta funkcja pobiera podany URL i przekształca go. Na przykład, jeśli nie chcesz podawać pełnego URL obrazu za każdym razem, możesz użyć zmienną afterLoaderImgUrl (albo prościej nazwę pliku) i przekazać ją bezpośrednio do ngSrc, tak jak na zdjęciu poniżej.

Angular oferuje kilka predefiniowanych loaderów, ale zawsze możesz stworzyć swój własny. Oczywiście, to tylko czubek góry lodowej. Aby lepiej zapoznać się ze wszystkimi możliwościami, które zapewnia ta dyrektywa, przeczytaj dokumentację NgOptimizedImage.

Podsumowanie

Ustawianie i praca z nową dyrektywą NgOptimizedImage jest niesamowicie proste. Dzięki jedynie importowaniu jej i implementacji jej sugestii, w znacznym stopniu poprawiłem ogólną wydajność mojej aplikacji. Ponadto, zajęło mi to tylko kilka minut, zamiast dni albo tygodni pracy, które mogłoby mi to zająć w innym przypadku. Dzięki dodatkom jak te, Angular kontynuuje swoje panowanie nad innymi frameworkami, jeśli chodzi o wydajność i przyjazność dla developerów. Nie mogę doczekać się, co zaoferuje w przyszłości!

O autorze

Kiril Zafirov

Senior Frontend Engineer focusing on Angular and Angular-related technologies. Organizer of the Angular Macedonia Meetup and the Angular Developer community.

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 *