Optymalizacja frontendu
<img> vs. background-image w nagłówku strony
W tym artykule przyjrzymy się różnym sposobom ładowania obrazka w przestrzeni hero / Above The Fold. Różne podejścia mogą przynieść naprawdę różne rezultaty wydajnościowe. Ważne jest, aby to, co znajduje się na samej górze strony, było ładowane jak najszybciej. Dzięki temu użytkownik będzie mógł zapoznawać się z treściami w sposób sprawny i będzie pozytywnie postrzegał progres ładowania.
Wszystkie testy, pomiary i wnioski w tym artykule mogą być kluczowe ze względu na metrykę o nazwie Largest Contentful Paint (LCP). Jest ona ważna z perspektywy postrzegalnej wydajności front-endu, gdyż odnosi się do czasu renderowania największego elementu contentowego w przestrzeni Above The Fold. Bardzo często to właśnie warstwa z osadzonym obrazkowym tłem będzie zaliczana jako LCP i należy uczynić ładowanie tego obrazka jak najsprawniejszym.
Zapoznaj się najpierw z poniższą tezą, którą na przestrzeni kolejnych pomiarów i analiz, będę starał się udowodnić.
Powinno się unikać background-image i najlepiej osadzać obrazek jako <img>. Nie powinno ładować się tego obrazka z zewnętrznego serwera, nawet jeśli jest to CDN, i nawet jeśli "rozgrzewamy" połączenie do tej domeny za pomocą resource hint'a w postaci preload lub preconnect w <head>. Pobieranie obrazka przez zewnętrzną usługę wydłuża czas oczekiwania na załadowanie tego zasobu.
Warto zauważyć, że obrazki, które serwowane są przez CDN w poniższych testach (ImageKit), dodatkowo ładowane są w formacie WebP, co znacząco zmniejsza ich rozmiar.
Testy przeprowadzone zostały poprzez WebPageTest, na połączeniu 3G Fast, z Frankfurtu, emulując "średnie" urządzenie Motorola G4.
#1 Osadzenie obrazka jako background-image (obrazek serwowany przez CDN), zdefiniowany w osobnym arkuszu CSS
W sekcji <head> znajduje się <link> do arkusza CSS, w którym osadzone zostało tło jako background-image (responsywne, per dana rozdzielczość).
Zwróć uwagę na waterfallu, kiedy rozpoczyna się pobieranie obrazka.
Przemyśl obecny stan rzeczy: aby wyświetlić tę stronę, najpierw musi zostać pobrany HTML, następnie HTMLowy parser napotka na <link> do arkusza CSS, który musi zostać pobrany i zinterpretowany. Dopiero po jego pobraniu, po "odkryciu" CSSowego bloku odpowiedzialnego za warstwę w HTML (czyli w momencie, gdy konstruowane będzie drzewo CSSOM), rozpocznie się pobieranie obrazka. Bardzo zła sytuacja.
#2 Osadzenie obrazka jako background-image (obrazek serwowany przez CDN), zdefiniowane w <style> w sekcji <head>)
W tym teście zobaczmy jak zachowa się to samo tło (serwowane przez CDN), ale ustawione po prostu inline'nowo w tagu <style> w sekcji <head>.
Demo: https://wdi-test-task2.vercel.app/hero-bg-image-style.html
Zwróć uwagę, kiedy ładowany jest obrazek względem CSS (na waterfallu) i porównaj z poprzednim testem.
Obrazek pobierany jest w tym samym czasie, gdy pobierany jest arkusz styli CSS.
#3 OSADZENIE OBRAZKA JAKO BACKGROUND-IMAGE (OBRAZEK HOSTOWANY lokalnie), ZDEFINIOWANE W <STYLE> W SEKCJI <HEAD> (dobre rozwiązanie!)
W tym teście zobaczmy jak zachowa się to samo tło, co w poprzednim przykładzie, ale hostowane lokalnie.
Demo: https://wdi-test-task2.vercel.app/hero-bg-image-style-local.html
Ten przykład prezentuje bardzo dobry rezultat.
#4 Osadzenie <img> z <picture> (obrazki serwowane przez CDN)
Rezultat bardzo podobny do poprzedniego z testu #2 (mówiąc chociażby o waterfallu), choć patrząc na czasy ładowania, ten przykład prezentuje nieco gorszy rezultat. Niemniej jednak rozpoczyna ładowanie obrazka nieco szybciej (zwróć uwagę na progres ładowania obrazka jako <img> względem background-image).
#5 Osadzenie <img> z <picture> (obrazki samodzielnie hostowane na serwerze)
Jak widzisz, samo serwowane obrazka z naszego serwera pozwoliło wyświetlić obrazek o ok. 0.5s szybciej. Zwróć uwagę na waterfall poniżej paska ze screenshotami i porównaj z poprzednim przykładem. Zauważ, ile czasu zajmuje faza nawigacji do zewnętrznego źródła (DNS lookup, TCP/TLS handshake). Dlatego właśnie odradzam ładowania hero image przez zewnętrzne usługi. Nie twierdzę jednocześnie, że CDN to zły pomysł - jest świetnym pomysłem i powinniśmy go stosować - ale raczej dla zasobów BTF.
#6 Osadzenie <img> z <picture> (obrazki serwowane przez CDN + zastosowanie LQIP)
LQIP, czyli Low Quality Image Placeholder - jest to najczęściej obrazek o bardzo małych rozmiarach względem oryginalnego, o słabej jakości, prezentujący jedynie swego rodzaju obrys i zawierający dominujące kolory. Jest to taka pierwsza wersja obrazka, która może zostać bardzo szybko wyświetlona użytkownikowi, w oczekiwaniu aż docelowy obrazek dopiero się załaduje.
Jak widzisz, docelowy obrazek serwowany jest późno, natomiast użytkownik tak czy siak widzi LQIP dużo szybciej (tym samym może już czytać tekst na nim). Niemniej jednak podany sposób nie przedstawia najlepszego rezultatu.
Porównaj ten przykład z pierwszym testem. Obrazek i w tym, i w pierwszym teście jest w pełni załadowany w 1.9 s, natomiast w powyższym przykładzie, dzięki LQIP, prezentowana jest użytkownikowi wypełniona przestrzeń, z tekstem, który użytkownik może czytać już po pierwszej sekundzie ładowania strony. Podobno efekt moglibyśmy osiągnąć osadzając po prostu background-color pod obrazkiem i też jest to jakiś pomysł, aby dać użytkownikowi możliwość zapoznania się z treścią jak najszybciej.
#7 Osadzenie <img> z <picture> (obrazki samodzielnie hostowane na serwerze + zastosowanie LQIP)
Zwróć uwagę na kod HTML tego przykładu i sposób implementacji LQIP. Obrazek o niskiej jakości został osadzony przy pomocy background-image (o którym wielokrotnie mówiłem, aby go raczej unikać), natomiast jeśli adresem URL jest zakodowany obrazek w postaci base64 - jest on mimo wszystko wtedy szybko renderowany. Jest to w zasadzie jedyny przykład zastosowania base64, z którym personalnie się zgadzam, że jest warty użycia.
Sam docelowy obrazek jest nadal hostowany na serwerze (tak jak w przykładzie nr 2) i nadal serwowany jest JPEG (nie WebP). Pomyśl o ile lepszy rezultat mógłby jeszcze być, gdyby rzeczywiście serwować obrazek w tym nowoczesnym, lekkim formacie.
Powyższe rozwiązanie jest z mojego punktu widzenia najlepszym i zapewnia najlepszą postrzegalną wydajność.
Werdykt / idealny sposób ładowania hero image:
- jeśli możesz, ładuj obrazek jako <img> / <picture> i unikaj background-image (chyba, że w znaczniku <style> jako krytyczny CSS - dzięki temu użytkownik nie będzie musiał czekać na zakończenie pobierania plików CSS); dodanie ukrytego <img> w środku tej warstwy + najlepiej dodatkowo <link rel="preload"> do sekcji <head> jest również jakimś rozwiązaniem. To pozwoli przeglądarce znaleźć ten zasób szybciej i szybciej go ładować
- zawsze ładuj obrazek hostowany lokalnie (bez użycia CDN) - unikniesz fazy nawigacji do zewnętrznej usługi (gdzie wymagana jest faza DNS lookup, TCP/TLS handshake)
- idealnie, gdyby obrazek był serwowany jako WebP wśród przeglądarek, które to obsługują (można to zautomatyzować - czyli serwujemy np. JPEG albo PNG a usługa jako całościowy CDN serwowałaby ten obrazek jako WebP, jeśli jest taka możliwość; niestety jest to możliwe do osiągnięcia na jednym z płatnych planów Cloudflare [usługa Polish])
- w hero <img> zastosuj LQIP w style="background-image: url()", idealnie zakodowany jako base64; jeśli z jakichś technicznych powodów nie chcesz stosować LQIP i kodowania base64 - zastosuj po prostu zwykłe background-color z jakimś manualnie dobranym, dominującym kolorem docelowego obrazka - nawet w takiej postaci usprawnimy postrzegalną wydajność!
- ładowanie obrazka jako <img> ma jeszcze jedną zaletę - obrazek jest znajdywany przez boty wyszukiwarek, dzięki czemu może być indeksowany w wynikach w wyszukiwania, a dodatkowo - jeśli jest prawidłowo opisany ALT textem - może być odczytywany przez czytniki ekranowe!
Poza tym, uważam, że powyższe wnioski powinny być rozważane również dla sekcji Below The Fold na stronie, gdzie również możemy mieć obrazkowe tła. Dzięki zastosowaniu <img> możemy to ładować jako tło (korzystając chociażby z CSS object-fit, a w dodatku dzięki img@srcset lub picture możemy ładować responsywne rozmiary względem rozdzielczości), zapewnić jego widoczność dla botów + technologii asystujących, a do tego ładować je łatwo w sposób leniwy.