DPI, DPR i srcset – co przeglądarka naprawdę widzi?

Przeglądarka ignoruje DPI zapisane w pliku obrazu. Jak więc decyduje, który obraz pobrać? Wyjaśniam, jak działają CSS px, Device Pixel Ratio i dlaczego srcset ma dwa deskryptory.

Jeśli trafiłeś tutaj z wpisu o DPI na blogu fotograficznym, to znaczy, że interesuje cię co się dzieje pod maską. W tamtym wpisie skończyliśmy na tym, że przeglądarka ignoruje DPI zapisane w pliku. Czas wyjaśnić, co zamiast tego czyta i jak to wpływa na to, jak serwujesz obrazy na stronie.


CSS px ≠ fizyczny piksel

Pierwsze i najważniejsze: px w CSS to nie jest fizyczny piksel ekranu. Nigdy nie było.

Według specyfikacji W3C 1 CSS px = 1/96 cala. Wszystkie inne absolutne jednostki długości bazują właśnie na tej definicji. Przeglądarka tłumaczy tę logiczną jednostkę na fizyczne piksele ekranu przez współczynnik skalowania – ale o tym za chwilę.

Konsekwencja jest taka, że przeglądarka od początku operuje na logicznych jednostkach, a nie fizycznych. DPI zapisane w metadanych pliku obrazu jest dla niej całkowicie niewidoczne i nieistotne.


Skąd się wzięło 96 DPI w CSS?

Krótka historia, bo warto znać kontekst zanim przejdziemy dalej.

Apple (1984) zdefiniowało ekrany o gęstości 72 PPI – nieprzypadkowo, bo w typografii 1 cal = 72 punkty typograficzne. Jeden piksel = jeden punkt typograficzny. To, co widziałeś na ekranie, odpowiadało dokładnie temu, co wychodziło z drukarki. WYSIWYG w pełnym tego słowa znaczeniu – Low End Mac opisuje to jako świadomą decyzję projektową, wymagającą m.in. pierwszego komputera zaprojektowanego specjalnie pod kwadratowe piksele.

Microsoft wybrał 96 DPI dla Windowsa, bo na ówczesnych monitorach CRT dawało to czytelniejsze czcionki. Gdy W3C standaryzowało CSS, zaadoptowało właśnie tę wartość. Jedna historyczna decyzja Microsoftu weszła do specyfikacji webowej i obowiązuje do dziś – jak pisze Microsoft Learn w dokumentacji High DPI, sami w końcu musieli od niej odejść w nowoczesnych wersjach Windows, żeby obsłużyć ekrany o wyższej gęstości.

„96 DPI w CSS to nie fizyka – to historyczna decyzja Microsoftu z lat 90., którą do dziś wszyscy szanujemy.”


Device Pixel Ratio – umowa projektowa

Skoro mamy logiczne piksele CSS, potrzebujemy czegoś, co tłumaczy je na fizyczne piksele ekranu. To jest Device Pixel Ratio (DPR) – współczynnik mówiący, ile fizycznych pikseli przypada na jeden CSS px.

Kilka przykładów:

  • Monitor FHD bez skalowania: DPR = 1
  • iPhone z Retina: DPR = 2 lub 3
  • Typowy Android flagship: DPR = 2.625 lub 3

Kluczowe jest to, jak to działa w praktyce: DPR to wartość projektowana przez producenta oprogramowania, a nie pomiar fizyczny. Producent telefonu lub systemu operacyjnego decyduje, jaki DPR zgłosi przeglądarce, biorąc pod uwagę gęstość ekranu, rozmiar urządzenia i ergonomię.

„DPR to umowa projektowa między hardware’em a systemem operacyjnym, a nie fizyczny pomiar.”

Przeglądarka zna DPR urządzenia przez właściwość window.devicePixelRatio. To jedyna informacja o “gęstości” jaką ma – fizycznej gęstości w PPI nie zna i nie może jej poznać.


srcset – skąd się wziął i dlaczego ma dwa deskryptory

Skoro przeglądarka rozumie DPR, potrzebujemy mechanizmu do serwowania różnych rozmiarów obrazu dla ekranów 1× i 3×. Tym mechanizmem jest srcset – ale żeby zrozumieć, dlaczego ma dwa różne deskryptory, warto znać jego historię.

Problem, który trzeba było rozwiązać (ok. 2010–2011)

Ekrany Retina trafiły do sprzedaży. Jednocześnie smartfony zaczęły generować znaczącą część ruchu webowego. Programiści trafili między młot a kowadło: na Retinie obraz wyglądał rozmazanie bo brakowało pikseli, a na telefonie z wolnym mobilnym internetem ten sam plik ważył kilka MB i ładował się wieczność. HTML nie oferował żadnego wbudowanego mechanizmu do serwowania różnych wersji obrazka – rozwiązywano to hackami w JavaScript i po stronie serwera.

Dwa niezależne pomysły i jeden kompromis (2011–2012)

Społeczność webdeveloperów zaczęła równolegle pracować nad dwoma podejściami. Jedno zakładało stworzenie nowego elementu <picture>, działającego jak <video> ze źródłami – przeglądarka wybiera plik na podstawie media queries. Drugie – zaproponowane przez ludzi powiązanych z WHATWG i Apple – rozszerzenie istniejącego <img> o nowy atrybut srcset. Oba podejścia trafiły do specyfikacji jako kompromis w 2012 roku. Cała historia jest dobrze udokumentowana przez The History of the Web, jeśli chcesz poczytać o kulisach.

Deskryptor x – pierwsza prosta wersja

Oryginalna składnia srcset używała deskryptora x oznaczającego wielokrotność gęstości pikseli:

<img
  src="logo.png"
  srcset="[email protected] 2x, [email protected] 3x"
  alt="Logo"
>

Przeglądarka patrzy na DPR urządzenia i pobiera odpowiedni plik. Proste, logiczne, rozwiązuje konkretny problem: Retina kontra zwykłe ekrany. Uznano to za właściwy pierwszy krok, a bardziej złożone przypadki miały przyjść po zebraniu doświadczeń z implementacji.

Problem z samym x – fluid layouts

Deskryptor x świetnie sprawdza się przy obrazach o stałym rozmiarze CSS – logo, awatar, ikona. Ale strony responsywne mają obrazki, których szerokość zmienia się płynnie z viewportem. Tutaj pojawia się subtelny, ale realny problem.

Gdy przeglądarka zaczyna pobierać zasoby, uruchamia preloader – mechanizm spekulatywnego pobierania obrazków jeszcze zanim w pełni przetworzy CSS i JavaScript. W tym momencie przeglądarka często nie wie jeszcze, jaki będzie końcowy rozmiar elementu na stronie. A bez tej wiedzy deskryptor x nie wystarczy.

Mówiąc prościej: “daj mi wersję 2×” to za mało, jeśli nie wiesz, ile CSS-owych pikseli zajmuje sam element.

Deskryptor w i atrybut sizes

Odpowiedzią okazała się para: deskryptor w opisujący rzeczywistą szerokość pliku w pikselach oraz atrybut sizes informujący przeglądarkę, jak duży będzie obraz w layoucie przy danym rozmiarze okna. MDN dokumentuje to dokładnie, a Smashing Magazine opisał ten mechanizm szczegółowo jeszcze w czasie, gdy pierwsze przeglądarki wdrażały implementacje.

<img
  src="zdjecie-800.jpg"
  srcset="zdjecie-800.jpg 800w, zdjecie-1600.jpg 1600w, zdjecie-2400.jpg 2400w"
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="Zdjęcie produktu"
>

Mając obie informacje, przeglądarka potrafi wyliczyć wszystko sama: viewport ma 1200px, obraz zajmie 50vw czyli 600 CSS px, DPR urządzenia wynosi 2, więc potrzebuję pliku o szerokości co najmniej 1200px – i wybiera zdjecie-1600.jpg. Dzieje się to w preloaderze, przed pełnym wyrenderowaniem strony.

W marcu 2017 srcset był w pełni obsługiwany przez wszystkie główne przeglądarki.

Krótkie podsumowanie: x było pierwsze, bo rozwiązywało prostszy i bardziej palący problem – Retina. w przyszło jako odpowiedź na pytanie “ale co z obrazkami, których rozmiar zależy od szerokości okna?” – i wymagało przy okazji sizes, żeby przeglądarka w ogóle wiedziała, jak duży obraz wybrać.


<picture> a <img> – czym się różnią?

Przy okazji warto rozróżnić te dwa elementy, bo często są mylone.

<img srcset sizes> to propozycja dla przeglądarki – dajesz jej listę wariantów i to ona decyduje, który pobrać, biorąc pod uwagę DPR, rozmiar okna i własne heurystyki. Ty sugerujesz, przeglądarka wybiera.

<picture> odwraca tę logikę – to Ty decydujesz, który plik zostanie użyty, bo warunki podajesz wprost przez media queries lub atrybut type. Przeglądarka wykonuje pierwszy pasujący <source> i nie kombinuje dalej. Polypane opisuje to jako art direction, a MDN ma pełny przewodnik po obu elementach.

Dwa przypadki, gdzie <picture> ma sens a <img srcset> nie wystarczy:

Art direction – chcesz pokazać zupełnie inny kadr na mobile niż na desktopie. Nie mniejszy plik, ale inne ujęcie:

<picture>
  <source media="(max-width: 600px)" srcset="zdjecie-portret.jpg">
  <source media="(min-width: 601px)" srcset="zdjecie-krajobraz.jpg">
  <img src="zdjecie-krajobraz.jpg" alt="...">
</picture>

Formaty z fallbackiem – chcesz serwować AVIF lub WebP tam, gdzie jest wsparcie, a JPEG wszędzie indziej. <img> nie ma atrybutu type, więc nie możesz warunkowo podać formatu inaczej niż przez <picture>:

<picture>
  <source type="image/avif" srcset="zdjecie.avif">
  <source type="image/webp" srcset="zdjecie.webp">
  <img src="zdjecie.jpg" alt="...">
</picture>

W praktyce <img srcset sizes> wystarczy do zdecydowanej większości przypadków. <picture> wchodzi wtedy, gdy potrzebujesz kontroli nad formatem albo nad kadrem. Wewnątrz <picture> zawsze musi być <img> na końcu – to fallback dla starszych przeglądarek i jedyne miejsce na atrybuty alt, class, loading itp.


Web vs aplikacje natywne

Na koniec jedna praktyczna różnica, którą warto znać jeśli piszesz coś więcej niż strony.

Przeglądarka zna tylko DPR – logiczny współczynnik skalowania. Nie ma dostępu do fizycznej gęstości pikseli ekranu. Dlatego nie możesz zbudować w przeglądarce idealnie dokładnej linijki w centymetrach bez kalibracji przez użytkownika.

Android (natywny) daje pełny dostęp do DisplayMetrics:

  • xdpi / ydpi → rzeczywista fizyczna gęstość pikseli zgłaszana przez producenta urządzenia
  • density → odpowiednik DPR, wartość logiczna używana do skalowania UI

Oznacza to, że aplikacja natywna na Androidzie może wyświetlić linijkę z centymetrami z sensowną dokładnością. Przeglądarka – nie, przynajmniej nie bez dodatkowego pytania użytkownika o kalibrację.


Podsumowanie

  • Przeglądarka ignoruje DPI z pliku. Operuje na logicznych CSS px, przeliczanych przez DPR.
  • DPR to umowa projektowa, nie fizyczny pomiar gęstości ekranu.
  • srcset z deskryptorem x – dla obrazów o stałym rozmiarze CSS (logo, ikony, awatary).
  • srcset z deskryptorem w + atrybut sizes – dla responsywnych obrazów o zmiennej szerokości.
  • <picture> daje Ci kontrolę; <img srcset> daje ją przeglądarce.
  • Natywne aplikacje mobilne widzą fizyczne PPI ekranu. Przeglądarka nie.

Powodzenia!


© 2024. Wszelkie prawa zastrzeżone.

Powered by Hydejack v9.2.1