Obrazy w wysokiej rozdzielczości dla zmiennej gęstości pikseli

Data publikacji: 22 sierpnia 2012 r., ostatnia aktualizacja: 14 kwietnia 2025 r.

Na rynku jest tak wiele urządzeń, że gęstość pikseli ekranu jest bardzo zróżnicowana. Deweloperzy aplikacji muszą obsługiwać różne gęstości pikseli, co może być dość trudne. W przypadku internetu mobilnego problemy te są potęgowane przez kilka czynników:

  • duża różnorodność urządzeń o różnych formatach.
  • ograniczona przepustowość sieci i czas pracy na baterii.

W przypadku obrazów celem programistów stron internetowych jest wyświetlanie obrazów o najwyższej jakości w jak najbardziej efektywny sposób. W tym artykule przedstawiamy przydatne techniki, które pomogą Ci w realizacji tych celów teraz i w przyszłości.

W miarę możliwości unikaj obrazów

Zanim założysz, że musisz dołączyć obraz, pamiętaj, że w internecie dostępne są liczne zaawansowane technologie, które są w dużej mierze niezależne od rozdzielczości i DPI. W szczególności tekst, SVG i duża część CSS „działają po prostu” dzięki automatycznej funkcji skalowania pikseli w internecie z devicePixelRatio.

Nie zawsze jednak można uniknąć obrazów rastrowych. Możesz na przykład otrzymać zasoby, które trudno byłoby odtworzyć w czystym formacie SVG lub CSS. Możesz mieć do czynienia ze zdjęciem. Możesz automatycznie przekonwertować obraz na SVG, ale wektoryzacja zdjęć ma niewielki sens, ponieważ powiększone wersje zwykle nie wyglądają dobrze.

Historia gęstości pikseli

W początkach wyświetlacze komputerów miały gęstość pikseli 72 lub 96punktów na cal (DPI).

Wyświetlacze stopniowo zwiększają gęstość pikseli, głównie dzięki postępowi w przypadku urządzeń mobilnych, ponieważ użytkownicy trzymają telefony bliżej twarzy, co sprawia, że piksele są bardziej widoczne. W 2008 roku telefony o rozdzielczości 150 dpi stały się nową normą. Wzrost gęstości pikseli na ekranie trwał dalej, a dzisiejsze telefony mają wyświetlacze o 300 dpi.

W praktyce obrazy o niskiej gęstości powinny wyglądać tak samo na nowych ekranach jak na starych, ale w porównaniu z ostrą grafiką o wysokiej gęstości, do której użytkownicy są przyzwyczajeni, obrazy o niskiej gęstości wyglądają dziwnie i pikselatowo. Poniżej przedstawiamy przybliżone symulacje tego, jak obraz w rozdzielczości 1x wygląda na wyświetlaczu w rozdzielczości 2x. Zdjęcie w rozdzielczości 2 x wygląda za to całkiem nieźle.

1 x 2 piksele
Baboon, gęstość pikseli 1×. Baboon 2x – gęstość pikseli 2 x.
Małpy na ekranach o różnej gęstości pikseli.

Piksele w internecie

W czasie projektowania internetu 99% wyświetlaczy miało rozdzielczość 96 dpi, a w przypadku odstępstw od tej zasady przewidziano niewiele wyjątków. Obecnie mamy duże zróżnicowanie rozmiarów i gęstości ekranów, dlatego potrzebujemy standardowego sposobu na to, aby obrazy dobrze wyglądały na wszystkich urządzeniach.

Specyfikacja HTML rozwiązała ten problem, definiując piksel referencyjny, którego producenci używają do określania rozmiaru piksela CSS.

Korzystając z pikselu referencyjnego, producent może określić rozmiar fizycznego piksela urządzenia w stosunku do piksela standardowego lub idealnego. Ten współczynnik nazywa się współczynnikiem pikseli urządzenia.

Obliczanie współczynnika pikseli urządzenia

Załóżmy, że telefon komórkowy ma ekran o fizycznym rozmiarze piksela 180 pikseli na cal (ppi). Obliczenie współczynnika pikseli urządzenia wymaga wykonania 3 czynności:

  1. Porównaj rzeczywistą odległość, w której trzymane jest urządzenie, z odległością dla piksela referencyjnego.

    Według specyfikacji przy 28 calach idealna liczba pikseli na cal to 96. Wiemy, że w przypadku telefonów komórkowych twarze użytkowników znajdują się bliżej urządzenia niż w przypadku laptopów i komputerów stacjonarnych. W przypadku poniższych równań szacujemy, że odległość wynosi 18 cali.

  2. Aby uzyskać idealną gęstość pikseli dla danej odległości, pomnóż współczynnik odległości przez standardową gęstość (96 ppi).

    idealPixelDensity = (28/18) × 96 = 150 pikseli na cal (w przybliżeniu)

  3. Aby uzyskać współczynnik pikseli urządzenia, należy wziąć iloraz fizycznej gęstości pikseli i idealnej gęstości pikseli.

    devicePixelRatio = 180/150 = 1.2

Przykładowy kątowy piksel, który obrazuje sposób obliczania współczynnika pikseli urządzenia.

Gdy przeglądarka musi określić, jak zmienić rozmiar obrazu, aby pasował do ekranu, zgodnie z idealną lub standardową rozdzielczością, przeglądarka odwołuje się do współczynnika pikseli urządzenia wynoszącego 1,2. Oznacza to, że na każde idealne piksel tego urządzenia przypada 1,2 fizycznego piksela. Oto formuła do przeliczania pikseli idealnych (zgodnie ze specyfikacją internetową) i fizycznych (punktów na ekranie urządzenia):

physicalPixels = window.devicePixelRatio * idealPixels

W przeszłości dostawcy urządzeń zaokrąglają devicePixelRatios(DPR). iPhone i iPad firmy Apple raportują DPR 1, a ich odpowiedniki Retina – 2. Specyfikacja usługi porównywania cen zaleca, aby

jednostka piksela odnosi się do liczby całkowitej pikseli urządzenia, która najlepiej przybliża się do piksela odniesienia.

Jednym z powodów, dla których proporcje okrągłe mogą być lepsze, jest to, że mogą one powodować mniej artefaktów poniżej piksela.

W rzeczywistości jednak urządzenia są znacznie bardziej zróżnicowane, a DPR telefonów z Androidem wynosi często 1, 5. Tablet Nexus 7 ma DPR ~1,33, który został obliczony w sposób podobny do tego w poprzednim przykładzie. W przyszłości spodziewaj się większej liczby urządzeń z rozmaitterm DPR. Z tego powodu nigdy nie zakładaj, że klienci mają liczby całkowite DPR.

Techniki dotyczące obrazów HiDPI

Istnieje wiele technik umożliwiających jak najszybsze wyświetlanie obrazów o najwyższej jakości. Można je ogólnie podzielić na 2 kategorie:

  1. optymalizowanie pojedynczych obrazów.
  2. Optymalizacja wyboru między wieloma obrazami.

Pojedyncze obrazy: użyj jednego obrazu, ale w sprytny sposób. Te metody mają tę wadę, że musisz poświęcić wydajność, ponieważ musisz pobierać obrazy w wysokiej rozdzielczości nawet na starszych urządzeniach o niższej rozdzielczości. Oto kilka metod na rozwiązanie problemu dotyczącego pojedynczego obrazu:

  • mocno skompresowany obraz HiDPI.
  • Niesamowicie fajny format obrazu
  • Progresywny format obrazu

Podejście z użyciem wielu obrazów: używaj wielu obrazów, ale wymyśl coś sprytnego, aby wybrać, które z nich mają się wczytać. Takie podejścia wymagają od dewelopera tworzenia wielu wersji tego samego komponentu, a następnie opracowania strategii podejmowania decyzji. Dostępne opcje to:

  • JavaScript
  • Dostarczanie po stronie serwera
  • Zapytania multimedialne w CSS
  • Wbudowane funkcje przeglądarki (image-set(), <img srcset>)

mocno skompresowany obraz HiDPI;

Obrazy stanowią już aż 60% przepustowości zużywanej na pobieranie przeciętnej witryny. Podając obrazy w wysokiej rozdzielczości wszystkim klientom, zwiększamy tę liczbę. O ile może się powiększyć?

Przeprowadziłem kilka testów, w których wyniku wygenerowano fragmenty obrazu w rozdzielczości 1x i 2x z jakością JPEG 90, 50 i 20.

6 wersji jednego obrazu, różniących się kompresją i gęstością pikseli. 6 wersji jednego obrazu, różniących się kompresją i gęstością pikseli. 6 wersji jednego obrazu, różniących się kompresją i gęstością pikseli.

Na podstawie tego małego, nienaukowego próbkowania wydaje się, że kompresowanie dużych obrazów zapewnia dobry kompromis między jakością a rozmiarem. Moim zdaniem obrazy skompresowane 2 x wyglądają lepiej niż nieskompresowane obrazy 1 x.

Jednak wyświetlanie na urządzeniach 2x obrazów o niskiej jakości, które są mocno skompresowane, jest gorsze niż wyświetlanie obrazów o wyższej jakości. Takie podejście powoduje nałożenie kar za jakość obrazu. Jeśli porównamy obrazy quality: 90 z obrazami quality: 20, zauważymy, że obrazy quality: 90 są mniej ostre i bardziej ziarniste. Artefakty z quality:20 mogą nie być akceptowalne w przypadku obrazów o wysokiej jakości (np. w aplikacji do przeglądania zdjęć) lub w przypadku deweloperów, którzy nie chcą iść na kompromis.

To porównanie zostało wykonane wyłącznie na podstawie skompresowanych plików JPEG. Warto pamiętać, że między powszechnie stosowanymi formatami obrazów (JPEG, PNG, GIF) istnieje wiele kompromisów, co prowadzi nas do…

WebP: świetny format obrazu

WebP to atrakcyjny format obrazu, który kompresuje obrazy bardzo dobrze, zachowując przy tym ich jakość.

Jednym ze sposobów sprawdzenia obsługi WebP jest użycie JavaScriptu. Wczytaj obraz o wymiarach 1 piksela za pomocą funkcji data-uri, odczekaj, aż zostaną wywołane zdarzenia „loaded” lub „error”, a potem sprawdź, czy rozmiar jest prawidłowy. Modernizr zawiera skrypt wykrywania funkcji, który jest dostępny w Modernizr.webp.

Lepszym sposobem jest jednak użycie bezpośrednio w CSS funkcji image(). Jeśli masz obraz WebP i JPEG jako alternatywę, możesz napisać:

#pic {
  background: image("foo.webp", "foo.jpg");
}

Takie podejście ma kilka wad. Po pierwsze, image() nie jest w ogóle szeroko stosowany. Po drugie, choć kompresja WebP przewyższa JPEG, to nadal jest to stosunkowo niewielkie ulepszenie – około 30% mniejsze niż w tym obrazie WebP. Dlatego WebP samo w sobie nie wystarczy, aby rozwiązać problem z wysoką rozdzielczością.

Progresywne formaty obrazów

Formaty obrazów progresywnych, takie jak JPEG 2000, JPEG progresywny, PNG progresywny i GIF, mają (co jest nieco dyskusyjne) tę zaletę, że obraz pojawia się na ekranie przed jego pełnym załadowaniem. Mogą one generować pewien nadmiar rozmiaru, chociaż istnieją sprzeczne dowody na ten temat. Jeff Atwood twierdził, że tryb progresywny „zwiększa rozmiar obrazów PNG o około 20%, a obrazów JPEG i GIF o około 10%”. Jednak Stojan Stefanov twierdzi, że w przypadku dużych plików tryb progresywny jest bardziej wydajny (w większości przypadków).

Na pierwszy rzut oka obrazy progresywne wydają się bardzo obiecujące w kontekście wyświetlania obrazów o najwyższej jakości w jak najkrótszym czasie. Oznacza to, że przeglądarka może przerwać pobieranie i dekodowanie obrazu, gdy tylko stwierdzi, że dodatkowe dane nie poprawią jakości obrazu (czyli wszystkie ulepszenia jakości są poniżej piksela).

Połączenia można szybko kończyć, ale ich ponowne uruchamianie jest często kosztowne. W przypadku witryny z dużą liczbą obrazów najskuteczniejszym podejściem jest utrzymywanie jednego połączenia HTTP, aby używać go tak długo, jak to możliwe. Jeśli połączenie zostanie przerwane przedwcześnie, ponieważ obraz został pobrany w wystarczającym stopniu, przeglądarka musi utworzyć nowe połączenie, co może być bardzo wolne w środowiskach o niskiej latencji.

Jednym ze sposobów obejścia tego problemu jest użycie żądania zakresu HTTP, które pozwala przeglądarkom określić zakres bajtów do pobrania. Inteligentna przeglądarka może wysłać żądanie HEAD, aby pobrać nagłówek, przetworzyć go, zdecydować, ile miejsca na obraz jest w rzeczywistości potrzebne, a następnie pobrać obraz. Niestety zakres HTTP jest słabo obsługiwany na serwerach WWW, co powoduje, że to podejście jest niepraktyczne.

Oczywistym ograniczeniem tego podejścia jest to, że nie można wybrać obrazu do załadowania, tylko różne poziomy jakości tego samego obrazu. W rezultacie nie dotyczy to przypadku użycia „kierowanie artysty”.

Używanie JavaScriptu do określania, który obraz ma być wczytany

Pierwszym i najbardziej oczywistym sposobem na określenie, który obraz ma być wczytany, jest użycie JavaScriptu w kliencie. Dzięki temu możesz dowiedzieć się wszystkiego o kliencie użytkownika i podjąć właściwe działania. Możesz określić współczynnik pikseli urządzenia za pomocą funkcji window.devicePixelRatio, uzyskać szerokość i wysokość ekranu, a nawet przeprowadzić sniffowanie połączenia sieciowego za pomocą navigator.connection lub wysyłając fałszywe żądanie, tak jak to robi biblioteka foresight.js. Gdy już zbierzesz wszystkie te informacje, możesz zdecydować, który obraz ma się wczytać.

Z tej techniki korzysta około miliona bibliotek JavaScriptu. Niestety żadna z nich nie wyróżnia się na tle innych.

Jednym z głównych minusów jest to, że wczytywanie obrazu opóźnia się do momentu zakończenia działania parsowania z wyprzedzeniem. Oznacza to, że obrazy nie zaczną się pobierać, dopóki nie zostanie wywołane zdarzenie pageload. Więcej informacji na ten temat znajdziesz w artykule Jasona Grigsby'ego.

Wybór obrazu do załadowania na serwer

Możesz odroczyć decyzję do strony serwera, pisząc niestandardowe przetwarzanie żądań dla każdego obrazu, który serwujesz. Takie przetwarzanie polegałoby na sprawdzeniu obsługi Retina na podstawie danych User-Agent (jedynych informacji udostępnianych serwerowi). Następnie, w zależności od tego, czy logika po stronie serwera chce wyświetlić zasoby HiDPI, wczytujesz odpowiedni zasób (nazwany zgodnie z jakiś znaną konwencją).

Niestety nagłówek User-Agent nie zawsze zawiera wystarczającą ilość informacji, aby określić, czy urządzenie powinno otrzymać obrazy o wysokiej czy niskiej jakości. Należy też unikać wszelkich rozwiązań, które używają funkcji User-Agent do podejmowania decyzji dotyczących stylu.

Używanie zapytań mediów w kodzie CSS

Zapytania o media w CSS są deklaratywnymi zapytaniami, które pozwalają określić intencję i pozwolić przeglądarce na odpowiednie działanie w Twoim imieniu. Oprócz najczęstszego zastosowania zapytań o multimedia, czyli dopasowania do rozmiaru urządzenia, możesz też stosować dopasowanie do devicePixelRatio. Powiązane zapytanie o multimedia to device-pixel-ratio. Jak można się spodziewać, ma ono powiązane warianty minimalny i maksymalny.

Jeśli chcesz wczytać obrazy o wysokiej rozdzielczości, a współczynnik pikseli urządzenia przekracza próg, wykonaj te czynności:

#my-image { background: (low.png); }

@media only screen and (min-device-pixel-ratio: 1.5) {
  #my-image { background: (high.png); }
}

Sprawa komplikuje się, gdy miesza się wszystkie prefiksy dostawców, zwłaszcza ze względu na duże różnice w umieszczaniu prefiksów „min” i „max”:

@media only screen and (min--moz-device-pixel-ratio: 1.5),
    (-o-min-device-pixel-ratio: 3/2),
    (-webkit-min-device-pixel-ratio: 1.5),
    (min-device-pixel-ratio: 1.5) {

  #my-image {
    background:url(high.png);
  }
}

Dzięki temu odzyskasz korzyści płynące z analizy wstępnej, które zostały utracone w rozwiązaniu JavaScript. Zyskasz też elastyczność w wybieraniu punktów przerwania w przypadku wersji responsywnych (np. możesz mieć obrazy o niskim, średnim i wysokim DPI), czego nie było możliwe przy podejściu po stronie serwera.

Niestety, jest ona nadal nieco niepraktyczna i prowadzi do dziwnie wyglądającego kodu CSS lub wymaga wstępnej obróbki. Ponadto to podejście jest ograniczone do właściwości CSS, więc nie ma możliwości ustawienia <img src>, a wszystkie obrazy muszą być elementami z tłem. Jeśli polegasz wyłącznie na współczynniku pikseli urządzenia, może się zdarzyć, że Twój telefon komórkowy o wysokiej rozdzielczości pobiera zasób graficzny o dużej rozdzielczości 2 x, gdy masz połączenie EDGE. Nie jest to najwygodniejsza opcja dla użytkowników.

Ponieważ image-set() jest funkcją CSS, nie rozwiązuje problemu w przypadku tagów <img>. Wpisz @srcset, aby rozwiązać ten problem. W następnej sekcji omówimy szczegółowo image-set i srcset.

Funkcje przeglądarki obsługujące wysoką rozdzielczość DPI

Ostatecznie sposób obsługi wysokiej rozdzielczości zależy od Twoich wymagań. Wszystkie powyższe podejścia mają wady.

Obecnie atrybuty image-set i srcset są powszechnie obsługiwane, więc są najlepszym rozwiązaniem. W przypadku starszych przeglądarek istnieją dodatkowe sprawdzone metody, które mogą nam pomóc.

Czym różnią się te dwa rozwiązania? image-set() to funkcja CSS, która może być używana jako wartość właściwości CSS background. srcset to atrybut specyficzny dla elementów <img> o podobnej składni. Oba te tagi umożliwiają określenie deklaracji obrazu, ale atrybut srcset pozwala też skonfigurować, który obraz ma się wczytać w zależności od rozmiaru widoku.

Sprawdzone metody dotyczące zestawów obrazów

Składnia image-set() przyjmuje co najmniej 1 deklarację obrazu oddzieloną przecinkami, która składa się z ciągu znaków adresu URL lub funkcji url(), po której następuje odpowiednia rozdzielczość. Na przykład:

image-set(
  url("image1.jpg") 1x,
  url("image2.jpg") 2x
);

/* You can also include image-set without `url()` */
image-set(
  "image1.jpg" 1x,
  "image2.jpg" 2x
);

To informuje przeglądarkę, że są 2 obrazy do wyboru. Jeden obraz jest zoptymalizowany pod kątem wyświetlaczy 1x, a drugi – 2x. Następnie przeglądarka wybiera, który z nich ma wczytać, na podstawie różnych czynników, w tym szybkości sieci (jeśli przeglądarka jest wystarczająco inteligentna).

Oprócz wczytania prawidłowego obrazu przeglądarka odpowiednio go przeskaluje. Innymi słowy, przeglądarka zakłada, że obrazy w standardowym rozmiarze są dwa razy większe niż w podwójnym, więc przeskaluje obraz w podwójnym rozmiarze o współczynnik 2, aby na stronie miał on taki sam rozmiar jak obraz w standardowym rozmiarze.

Zamiast wartości 1x, 1, 5x lub Nx możesz też określić gęstość pikseli urządzenia w DPI.

Jeśli martwisz się o starsze przeglądarki, które nie obsługują właściwości image-set, możesz dodać wartość zastępczą, aby mieć pewność, że obraz się wyświetli. Na przykład:

/* Fallback support. */
background-image: url(icon1x.jpg);
background-image: image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

Ten przykładowy kod wczytuje odpowiedni zasób w przeglądarkach, które obsługują image-set, a w przeciwnym razie przełącza się na zasób 1x.

Zastanawiasz się może, dlaczego nie użyć polyfill (czyli nie stworzyć shimu JavaScriptu) dla image-set()? Okazało się, że implementacja wydajnych polyfilli dla funkcji CSS jest dość trudna. (szczegóły znajdziesz w tym omówieniu w stylu www).

Srcset obrazu

Oprócz deklaracji, które udostępnia element image-set, element srcset przyjmuje też wartości szerokości i wysokości odpowiadające rozmiarowi widoku, starając się wyświetlić najbardziej odpowiednią wersję.

<img alt="my awesome image"
  src="banner.jpeg"
  srcset="banner-HD.jpeg 2x, banner-phone.jpeg 640w, banner-phone-HD.jpeg 640w 2x">

W tym przykładzie banner-phone.jpeg jest wyświetlany na urządzeniach z szerokością widoku mniejszą niż 640 pikseli, banner-phone-HD.jpeg na urządzeniach z małym ekranem i wysoką rozdzielczością, banner-HD.jpeg na urządzeniach z wysoką rozdzielczością i ekranem większym niż 640 pikseli oraz banner.jpeg na wszystkich pozostałych urządzeniach.

Używanie zestawu obrazów w przypadku elementów graficznych

Możesz mieć ochotę zastąpić elementy img elementami <div>z tłem i wykorzystać podejście z zestawem obrazów. To działa, ale z pewnymi zastrzeżeniami. Wadą jest to, że tag <img> ma długotrwałą wartość semantyczną. W praktyce jest to ważne z punktu widzenia ułatwień dostępu i robotów internetowych.

Możesz użyć właściwości CSS content, która automatycznie skaluje obraz na podstawie devicePixelRation. Na przykład:

<div id="my-content-image"
  style="content: -webkit-image-set(
    url(icon1x.jpg) 1x,
    url(icon2x.jpg) 2x);">
</div>

polyfill srcset

Przydatną funkcją srcset jest to, że zawiera naturalne wartości domyślne. Jeśli atrybut srcset nie jest zaimplementowany, wszystkie przeglądarki wiedzą, że należy przetworzyć atrybut src. Ponieważ jest to tylko atrybut HTML, można tworzyć polyfille za pomocą JavaScriptu.

Ta funkcja polyfill jest dostarczana z testami jednostkowymi, aby zapewnić jak największe dopasowanie do specyfikacji. Oprócz tego w przypadku implementacji natywnej atrybutu srcset polyfill nie może wykonywać żadnego kodu.

Podsumowanie

Najlepszym rozwiązaniem w przypadku obrazów o wysokiej rozdzielczości jest użycie plików SVG i CSS. Nie zawsze jest to jednak realistyczne rozwiązanie, zwłaszcza w przypadku witryn z dużą liczbą obrazów.

Rozwiązania oparte na JavaScript, CSS i serwerze mają swoje mocne i słabe strony. Najbardziej obiecujące podejście to użycie znaczników image-setsrcset.

Podsumowując, moje zalecenia są następujące: