Komponent z obrazem zawiera sprawdzone metody dotyczące skuteczności i zapewnia gotowe rozwiązanie do optymalizacji obrazów.
Obrazy są częstym źródłem wąskich gardeł wydajności w przypadku aplikacji internetowych i kluczowym obszarem optymalizacji. Nieoptymalizowane obrazy zwiększają rozmiar strony i stanowią ponad 70% jej łącznej wagi w bajtach na 90th percentylu. Wiele sposobów optymalizacji obrazów wymaga inteligentnego „komponentu obrazu” z wbudowanymi domyślnie rozwiązaniami zwiększającymi wydajność.
Zespół Aurora współpracował z Next.js przy tworzeniu jednego z takich komponentów. Celem było utworzenie zoptymalizowanego szablonu obrazu, który programiści internetowi mogliby dalej dostosowywać. Ten komponent stanowi dobry model i wyznacza standard tworzenia komponentów obrazów w innych platformach, systemach zarządzania treścią (CMS) i technologiach. Podobny komponent dla Nuxt.js powstał w ramach współpracy z nami. Pracujemy też z Angular nad optymalizacją obrazów w przyszłych wersjach. W tym poście omówimy, jak zaprojektowaliśmy komponent Next.js Image, i podzielimy się wnioskami, które wyciągnęliśmy w trakcie tego procesu.

Problemy i możliwości związane z optymalizacją obrazów
Grafika wpływa nie tylko na skuteczność, ale także na działalność. Liczba obrazów na stronie była drugim najlepszym predyktorem konwersji użytkowników odwiedzających witryny. Sesje, w których użytkownicy dokonali konwersji, zawierały o 38% mniej obrazów niż sesje, w których nie dokonali konwersji. W ramach audytu sprawdzonych metod Lighthouse podaje wiele możliwości optymalizacji obrazów i poprawy podstawowych sygnałów internetowych. Oto niektóre typowe obszary, w których obrazy mogą wpływać na podstawowe wskaźniki internetowe i wrażenia użytkowników:
Obrazy bez określonego rozmiaru pogarszają CLS
Obrazy wyświetlane bez podania ich rozmiaru mogą powodować niestabilność układu i przyczyniać się do wysokiego skumulowanego przesunięcia układu (CLS). Ustawienie atrybutów width i height w elementach <img> może pomóc w zapobieganiu przesunięciom układu. Na przykład:
<img src="flower.jpg" width="360" height="240">
Szerokość i wysokość powinny być ustawione tak, aby format renderowanego obrazu był zbliżony do jego naturalnego formatu. Znaczna różnica w formacie obrazu może spowodować, że obraz będzie wyglądać na zniekształcony. Stosunkowo nowa właściwość, która umożliwia określenie proporcji w CSS, może pomóc w dostosowywaniu rozmiaru obrazów do różnych urządzeń przy jednoczesnym zapobieganiu przesunięciom układu.
Duże obrazy mogą negatywnie wpływać na LCP
Im większy rozmiar pliku obrazu, tym dłużej trwa jego pobieranie. Duży obraz może być obrazem „hero” na stronie lub najważniejszym elementem w widocznym obszarze, który odpowiada za wywołanie największego wyrenderowania treści (LCP). Obraz, który jest częścią krytycznych treści i długo się pobiera, opóźni LCP.
W wielu przypadkach deweloperzy mogą zmniejszyć rozmiar obrazów dzięki lepszej kompresji i używaniu obrazów responsywnych. Atrybuty srcset i sizes elementu <img> pomagają udostępniać pliki obrazów o różnych rozmiarach. Przeglądarka może wtedy wybrać odpowiedni obraz w zależności od rozmiaru i rozdzielczości ekranu.
Słaba kompresja obrazu może negatywnie wpływać na LCP
Nowoczesne formaty obrazów, takie jak AVIF czy WebP, mogą zapewniać lepszą kompresję niż powszechnie używane formaty JPEG i PNG. Lepsza kompresja zmniejsza rozmiar pliku o 25–50% przy zachowaniu tej samej jakości obrazu. Dzięki temu pobieranie jest szybsze i zużywa mniej danych. Aplikacja powinna wyświetlać nowoczesne formaty plików graficznych w przeglądarkach, które je obsługują.
Wczytywanie niepotrzebnych obrazów pogarsza LCP
Obrazy znajdujące się poniżej linii podziału lub poza obszarem widocznym nie są wyświetlane użytkownikowi po załadowaniu strony. Można je odłożyć, aby nie wpływały na LCP i nie opóźniały go. Leniwe ładowanie można wykorzystać do późniejszego wczytywania takich obrazów, gdy użytkownik przewija stronę w ich kierunku.
Problemy z optymalizacją
Zespoły mogą ocenić koszt wydajności wynikający z wcześniej wymienionych problemów i wdrożyć sprawdzone rozwiązania, aby je przezwyciężyć. W praktyce jednak często tak się nie dzieje, a nieefektywne obrazy nadal spowalniają działanie internetu. Możliwe przyczyny:
- Priorytety: deweloperzy stron internetowych zwykle koncentrują się na optymalizacji kodu, JavaScriptu i danych. Dlatego mogą nie wiedzieć o problemach z obrazami ani o tym, jak je zoptymalizować. Obrazy utworzone przez grafików lub przesłane przez użytkowników mogą nie znajdować się wysoko na liście priorytetów.
- Gotowe rozwiązanie: nawet jeśli deweloperzy znają niuanse optymalizacji obrazów, brak gotowego rozwiązania „wszystko w jednym” dla ich platformy lub stosu technologii może ich zniechęcić.
- Obrazy dynamiczne: oprócz obrazów statycznych, które są częścią aplikacji, obrazy dynamiczne są przesyłane przez użytkowników lub pochodzą z zewnętrznych baz danych lub systemów CMS. Określenie rozmiaru takich obrazów może być trudne, ponieważ źródło obrazu jest dynamiczne.
- Nadmiar znaczników: rozwiązania umożliwiające uwzględnienie rozmiaru obrazu lub
srcsetdla różnych rozmiarów wymagają dodatkowych znaczników dla każdego obrazu, co może być uciążliwe. Atrybutsrcsetzostał wprowadzony w 2014 roku, ale obecnie korzysta z niego tylko 26,5% witryn. W przypadku korzystania zsrcsetdeweloperzy muszą tworzyć obrazy w różnych rozmiarach. Pomocne mogą być narzędzia takie jak just-gimme-an-img, ale trzeba ich używać ręcznie w przypadku każdego obrazu. - Obsługa przez przeglądarki: nowoczesne formaty obrazów, takie jak AVIF i WebP, tworzą mniejsze pliki graficzne, ale wymagają specjalnej obsługi w przeglądarkach, które ich nie obsługują. Deweloperzy muszą stosować strategie takie jak negocjacja treści lub element
<picture>, aby obrazy były wyświetlane we wszystkich przeglądarkach. - Problemy z leniwym ładowaniem: istnieje wiele technik i bibliotek, które umożliwiają wdrożenie leniwego ładowania obrazów w części strony widocznej po przewinięciu. Wybór najlepszego może być trudny. Deweloperzy mogą też nie wiedzieć, w jakiej odległości od „zagięcia” najlepiej wczytywać odroczone obrazy. Dodatkowo komplikują to różne rozmiary obszaru wyświetlania na urządzeniach.
- Zmieniająca się sytuacja: w miarę jak przeglądarki zaczynają obsługiwać nowe funkcje HTML lub CSS, aby zwiększyć wydajność, deweloperom może być trudno ocenić każdą z nich. Na przykład Chrome wprowadza funkcję Priorytet pobierania w ramach wersji próbnej origin. Może służyć do zwiększania priorytetu określonych obrazów na stronie. Deweloperom łatwiej byłoby wprowadzać takie ulepszenia, gdyby były one oceniane i wdrażane na poziomie komponentu.
Komponent obrazu jako rozwiązanie
Możliwości optymalizacji obrazów i trudności związane z wdrażaniem ich w każdej aplikacji z osobna doprowadziły nas do pomysłu na komponent obrazu. Komponent obrazu może obejmować sprawdzone metody i wymuszać ich stosowanie. Zastępując element <img> komponentem obrazu, deweloperzy mogą skuteczniej rozwiązywać problemy z wydajnością obrazu.
W ostatnim roku współpracowaliśmy z zespołem Next.js przy projektowaniu i wdrażaniu ich komponentu Image. Można go używać jako zamiennika istniejących elementów <img> w aplikacjach Next.js w ten sposób:
// Before with <img> element:
function Logo() {
return <img src="/logo.jpg" alt="logo" height="200" width="100" />
}
// After with image component:
import Image from 'next/image'
function Logo() {
return <Image src="/logo.jpg" alt="logo" height="200" width="100" />
}
Komponent ten próbuje rozwiązywać problemy związane z obrazami w sposób ogólny, korzystając z bogatego zestawu funkcji i zasad. Zawiera też opcje, które umożliwiają deweloperom dostosowywanie go do różnych wymagań dotyczących obrazów.
Ochrona przed przesunięciami układu
Jak już wspominaliśmy, obrazy bez określonych wymiarów powodują przesunięcia układu i zwiększają CLS. Gdy deweloperzy używają komponentu Image Next.js, muszą podać rozmiar obrazu za pomocą atrybutów width i height, aby zapobiec przesunięciom układu. Jeśli rozmiar jest nieznany, deweloperzy muszą określić layout=fill, aby wyświetlać obraz bez określonego rozmiaru w kontenerze o określonym rozmiarze. Możesz też użyć importowania obrazów statycznych, aby pobrać rozmiar rzeczywistego obrazu na dysku twardym w czasie kompilacji i uwzględnić go w obrazie.
// Image component with width and height specified
<Image src="/logo.jpg" alt="logo" height="200" width="100" />
// Image component with layout specified
<Image src="/hero.jpg" layout="fill" objectFit="cover" alt="hero" />
// Image component with image import
import Image from 'next/image'
import logo from './logo.png'
function Logo() {
return <Image src={logo} alt="logo" />
}
Deweloperzy nie mogą używać komponentu Image bez określenia rozmiaru, więc projekt zapewnia, że poświęcą czas na rozważenie rozmiaru obrazu i zapobiegną przesunięciom układu.
Ułatwianie reagowania
Aby obrazy były elastyczne na różnych urządzeniach, deweloperzy muszą ustawić atrybuty srcset i sizes w elemencie <img>. Chcieliśmy zmniejszyć ten wysiłek za pomocą komponentu Obraz. Komponent Next.js Image został zaprojektowany tak, aby ustawiać wartości atrybutów tylko raz na aplikację. Stosujemy je do wszystkich instancji komponentu Obraz w zależności od trybu układu. Opracowaliśmy rozwiązanie składające się z 3 części:
- Właściwość
deviceSizes: można jej użyć do jednorazowego skonfigurowania punktów przerwania na podstawie urządzeń powszechnie używanych przez użytkowników aplikacji. W pliku konfiguracyjnym znajdują się domyślne wartości punktów przerwania. - Właściwość
imageSizes: jest to też konfigurowalna właściwość służąca do uzyskiwania rozmiarów obrazów odpowiadających punktom przerwania rozmiaru urządzenia. layoutw każdym obrazie: służy do określania sposobu użycia właściwoścideviceSizesiimageSizesw przypadku każdego obrazu. Obsługiwane wartości trybu układu tofixed,fill,intrinsiciresponsive.
Gdy obraz jest żądany w trybach układu elastycznym lub wypełniania, Next.js określa obraz, który ma być wyświetlany, na podstawie rozmiaru urządzenia, które wysyła żądanie strony, i odpowiednio ustawia wartości srcset i sizes w obrazie.
Poniższe porównanie pokazuje, jak tryb układu może być używany do kontrolowania rozmiaru obrazu na różnych ekranach. Użyliśmy zdjęcia demonstracyjnego udostępnionego w dokumentacji Next.js, wyświetlanego na telefonie i standardowym laptopie.
| Ekran laptopa | Ekran telefonu |
|---|---|
| Layout = Intrinsic: zmniejsza się, aby dopasować do szerokości kontenera na mniejszych widocznych obszarach. Nie powiększa się poza swój naturalny rozmiar w większym widocznym obszarze. Szerokość kontenera wynosi 100% | |
|
|
| Układ = stały: obraz nie jest responsywny. Szerokość i wysokość są stałe, podobnie jak w przypadku elementu |
|
|
|
| Układ = Elastyczny: skalowanie w dół lub w górę w zależności od szerokości kontenera w różnych obszarach wyświetlania przy zachowaniu proporcji. | |
|
|
Układ = Wypełnij: szerokość i wysokość rozciągnięte tak, aby wypełnić kontener nadrzędny. (W tym przykładzie szerokość elementu nadrzędnego <div> wynosi 300*500)
|
|
|
|
Wbudowane leniwe ładowanie
Komponent Image domyślnie udostępnia wbudowane, wydajne rozwiązanie leniwego ładowania. Podczas korzystania z elementu <img> istnieje kilka opcji leniwego ładowania, ale wszystkie mają wady, które utrudniają ich użycie. Deweloper może zastosować jedną z tych metod leniwego ładowania:
- Określ atrybut
loading: jest on obsługiwany przez wszystkie nowoczesne przeglądarki. - Użyj interfejsu Intersection Observer API: tworzenie niestandardowego rozwiązania do leniwego ładowania wymaga wysiłku oraz przemyślanego projektu i wdrożenia. Deweloperzy nie zawsze mają na to czas.
- Zaimportuj bibliotekę innej firmy, aby leniwie wczytywać obrazy: może to wymagać dodatkowego wysiłku w celu oceny i zintegrowania odpowiedniej biblioteki innej firmy do leniwego wczytywania.
W komponencie Image Next.js ładowanie jest domyślnie ustawione na "lazy". Leniwe ładowanie jest realizowane za pomocą interfejsu Intersection Observer, który jest dostępny w większości nowoczesnych przeglądarek. Deweloperzy nie muszą podejmować żadnych dodatkowych działań, aby ją włączyć, ale w razie potrzeby mogą ją wyłączyć.
Wstępne wczytywanie ważnych obrazów
Elementy LCP to często obrazy, a duże obrazy mogą opóźniać LCP. Warto wstępnie wczytać najważniejsze obrazy, aby przeglądarka mogła je szybciej wykryć. Jeśli używasz elementu <img>, w sekcji head kodu HTML możesz umieścić wskazówkę dotyczącą wstępnego wczytywania w ten sposób:
<link rel="preload" as="image" href="important.png">
Dobrze zaprojektowany komponent obrazu powinien umożliwiać dostosowywanie sekwencji wczytywania obrazów niezależnie od używanej platformy. W przypadku komponentu obrazu Next.js deweloperzy mogą wskazać obraz, który jest dobrym kandydatem do wstępnego wczytania, za pomocą atrybutu priority komponentu obrazów.
<Image src="/hero.jpg" alt="hero" height="400" width="200" priority />
Dodanie atrybutu priority upraszcza znaczniki i jest wygodniejsze w użyciu. Deweloperzy komponentów obrazu mogą też sprawdzić opcje zastosowania heurystyki do automatyzacji wstępnego wczytywania obrazów widocznych na stronie bez przewijania, które spełniają określone kryteria.
Zachęcanie do hostowania obrazów o wysokiej wydajności
Sieci CDN do obsługi obrazów są zalecane do automatyzacji optymalizacji obrazów. Obsługują też nowoczesne formaty obrazów, takie jak WebP i AVIF. Komponent obrazu Next.js domyślnie korzysta z sieci CDN obrazów za pomocą architektury modułu wczytującego. Przykład poniżej pokazuje, że moduł wczytujący umożliwia skonfigurowanie CDN w pliku konfiguracyjnym Next.js.
module.exports = {
images: {
loader: 'imgix',
path: 'https://ImgApp/imgix.net',
},
}
Dzięki tej konfiguracji deweloperzy mogą używać względnych adresów URL w źródle obrazu, a platforma połączy względny adres URL ze ścieżką CDN, aby wygenerować bezwzględny adres URL. Obsługiwane są popularne sieci CDN do obsługi obrazów, takie jak Imgix, Cloudinary i Akamai. Architektura obsługuje korzystanie z niestandardowego dostawcy chmury przez wdrożenie niestandardowej funkcji loader w aplikacji.
Obsługa obrazów hostowanych samodzielnie
W niektórych sytuacjach witryny nie mogą używać sieci CDN do obsługi obrazów. W takich przypadkach komponent obrazu musi obsługiwać obrazy hostowane samodzielnie. Komponent obrazu Next.js używa optymalizatora obrazów jako wbudowanego serwera obrazów, który udostępnia interfejs API podobny do CDN. Optymalizator używa biblioteki Sharp do przekształcania obrazów produkcyjnych, jeśli jest ona zainstalowana na serwerze. Ta biblioteka to dobry wybór dla każdego, kto chce stworzyć własny potok optymalizacji obrazów.
Obsługa progresywnego wczytywania
Progresywne ładowanie to technika, która ma na celu utrzymanie zainteresowania użytkowników poprzez wyświetlanie obrazu zastępczego o znacznie niższej jakości podczas ładowania rzeczywistego obrazu. Poprawia to postrzeganą wydajność i zwiększa wygodę użytkowników. Można go używać w połączeniu z leniwym ładowaniem obrazów widocznych po przewinięciu strony lub obrazów widocznych bez przewijania.
Komponent Next.js Image obsługuje progresywne wczytywanie obrazu za pomocą właściwości placeholder. Może to być LQIP (Low-quality image placeholder), czyli obraz zastępczy niskiej jakości, który wyświetla rozmyty obraz podczas ładowania rzeczywistego obrazu.
Wpływ
Po wprowadzeniu tych wszystkich optymalizacji komponent obrazu Next.js sprawdził się w środowisku produkcyjnym. Pracujemy też nad podobnymi komponentami obrazu w przypadku innych stosów technologii.
Gdy Leboncoin przeniosło starszą wersję frontendu JavaScript na Next.js, uaktualniło też potok obrazów, aby używać komponentu Image Next.js. Na stronie, która została przeniesiona z <img> na następny/obraz, wartość LCP spadła z 2,4 s do 1,7 s. Łączna liczba bajtów obrazów pobranych na stronę zmniejszyła się z 663 KB do 326 KB (z około 100 KB bajtów obrazów wczytywanych na żądanie).
Wyciągnięte wnioski
Każda osoba tworząca aplikację Next.js może skorzystać z komponentu Next.js Image w celu optymalizacji. Jeśli jednak chcesz utworzyć podobne abstrakcje wydajności dla innej platformy lub systemu CMS, poniżej znajdziesz kilka wniosków, które mogą Ci się przydać.
Zawory bezpieczeństwa mogą przynieść więcej szkody niż pożytku
We wczesnej wersji komponentu Image Next.js udostępniliśmy atrybut unsized, który umożliwiał programistom pominięcie wymagania dotyczącego rozmiaru i używanie obrazów o nieokreślonych wymiarach. Uznaliśmy, że będzie to konieczne w przypadkach, gdy nie można z wyprzedzeniem określić wysokości lub szerokości obrazu. Zauważyliśmy jednak, że użytkownicy w przypadku problemów z wymaganiami dotyczącymi rozmiaru w zgłoszeniach w GitHubie zalecają atrybut unsized jako uniwersalne rozwiązanie, nawet w sytuacjach, w których można rozwiązać problem w sposób, który nie pogorszy CLS. Następnie wycofaliśmy i usunęliśmy atrybut unsized.
Oddzielanie przydatnych utrudnień od bezsensownych niedogodności
Wymóg dotyczący rozmiaru obrazu jest przykładem „przydatnego tarcia”. Ogranicza to użycie komponentu, ale w zamian zapewnia nieproporcjonalnie duże korzyści w zakresie wydajności. Użytkownicy chętnie zaakceptują to ograniczenie, jeśli będą mieli jasny obraz potencjalnych korzyści związanych ze skutecznością. Dlatego warto wyjaśnić ten kompromis w dokumentacji i innych opublikowanych materiałach dotyczących komponentu.
Możesz jednak znaleźć rozwiązania, które pozwolą Ci uniknąć tych problemów bez obniżania skuteczności. Na przykład podczas tworzenia komponentu obrazu Next.js otrzymaliśmy skargi, że wyszukiwanie rozmiarów obrazów przechowywanych lokalnie jest uciążliwe. Dodaliśmy importowanie obrazów statycznych, które usprawnia ten proces, automatycznie pobierając wymiary lokalnych obrazów w czasie kompilacji za pomocą wtyczki Babel.
Zachowaj równowagę między wygodnymi funkcjami a optymalizacją wydajności
Jeśli komponent obrazu nie robi nic poza narzucaniem użytkownikom „przydatnych utrudnień”, deweloperzy raczej nie będą chcieli go używać. Okazało się, że najważniejsze są funkcje związane z wydajnością, takie jak rozmiar obrazu i automatyczne generowanie wartości srcset. Zainteresowanie komponentem obrazu Next.js wzbudziły też funkcje ułatwiające pracę deweloperom, takie jak automatyczne leniwe ładowanie i wbudowane rozmyte symbole zastępcze.
Określ plan rozwoju funkcji, aby zwiększyć ich popularność
Stworzenie rozwiązania, które sprawdzi się idealnie w każdej sytuacji, jest bardzo trudne. Może się zdarzyć, że zaprojektujesz coś, co będzie dobrze działać w przypadku 75% osób, a pozostałym 25% powiesz, że „w tych przypadkach ten komponent nie jest dla Ciebie”.
W praktyce ta strategia okazuje się sprzeczna z Twoimi celami jako projektanta komponentów. Chcesz, aby deweloperzy zaczęli używać Twojego komponentu, aby korzystać z jego zalet związanych z wydajnością. Jest to trudne, jeśli część użytkowników nie może przejść na nową platformę i czuje się wykluczona z rozmowy. Prawdopodobnie wyrażą rozczarowanie, co doprowadzi do negatywnych opinii, które wpłyną na wdrożenie.
Warto mieć plan rozwoju komponentu, który obejmuje wszystkie uzasadnione przypadki użycia w dłuższej perspektywie. W dokumentacji warto też wyraźnie zaznaczyć, co nie jest obsługiwane i dlaczego, aby określić oczekiwania dotyczące problemów, które ma rozwiązywać komponent.
Podsumowanie
Korzystanie z obrazów i ich optymalizacja są skomplikowane. Deweloperzy muszą znaleźć równowagę między wydajnością a jakością obrazów, zapewniając jednocześnie użytkownikom świetne wrażenia. Dlatego optymalizacja obrazów jest kosztownym, ale bardzo skutecznym działaniem.
Zamiast za każdym razem wymyślać wszystko od nowa, stworzyliśmy szablon sprawdzonych metod, z którego deweloperzy, platformy i inne technologie mogą korzystać jako punktu odniesienia przy własnych wdrożeniach. To rozwiązanie z pewnością okaże się przydatne, gdy będziemy obsługiwać inne platformy w przypadku ich komponentów obrazu.
Komponent Image w Next.js skutecznie poprawia wydajność aplikacji Next.js, a tym samym zwiększa komfort użytkowników. Uważamy, że to świetny model, który sprawdzi się w szerszym ekosystemie. Chętnie poznamy opinie deweloperów, którzy chcieliby go zastosować w swoich projektach.