Images haute résolution pour des densités de pixels variables

Dans le paysage complexe des appareils d'aujourd'hui, il existe une très large gamme de densités de pixels d'écran. Certains appareils sont dotés d'un écran très haute résolution, tandis que d'autres sont à court d'éléments. Les développeurs d'applications doivent prendre en charge une plage de densités de pixels, ce qui peut s'avérer assez difficile. Sur le Web mobile, les défis sont aggravés par plusieurs facteurs:

  • Grande variété d'appareils avec différents facteurs de forme.
  • Bande passante réseau et autonomie limitées.

En termes d'images, l'objectif des développeurs d'applications Web est de diffuser des images de la meilleure qualité possible le plus efficacement possible. Cet article présente quelques techniques utiles permettant d'effectuer cette opération dès aujourd'hui et dans un avenir proche.

Éviter les images si possible

Avant d'ouvrir cette boîte de vers, n'oubliez pas que le Web dispose de nombreuses technologies puissantes qui sont en grande partie indépendantes de la résolution et de la DPI. Plus précisément, le texte, le format SVG et une grande partie du code CSS "fonctionneront" grâce à la fonctionnalité Web de mise à l'échelle automatique des pixels (via devicePixelRatio).

Cela dit, vous ne pouvez pas toujours éviter les images matricielles. Par exemple, vous pouvez recevoir des éléments qui sont assez difficiles à reproduire en SVG/CSS pur, ou si vous avez affaire à une photo. Bien que vous puissiez convertir l'image au format SVG automatiquement, la vectorisation des photos n'a pas de sens, car les versions agrandies ne sont généralement pas attrayantes.

Contexte

Un très court historique de la densité d'affichage

Au début, les écrans d'ordinateur avait une densité de pixels de 72 ou 96 ppp (points par pouce).

Les écrans ont progressivement amélioré la densité de pixels, en grande partie en raison du cas d'utilisation mobile, dans lequel les utilisateurs tiennent généralement leurs téléphones plus près de leur visage, ce qui rend les pixels plus visibles. En 2008, les téléphones à 150 ppp étaient la nouvelle norme. La tendance d'augmentation de la densité d'affichage s'est poursuivie, et les nouveaux téléphones d'aujourd'hui sont dotés d'écrans 300 ppp (appelés "Retina" d'Apple).

Le Saint Graal, bien sûr, est un affichage dans lequel les pixels sont complètement invisibles. En ce qui concerne le facteur de forme des téléphones, la génération actuelle d'écrans Retina/HiDPI pourrait s'en rapprocher. Toutefois, les nouvelles catégories de matériel et d'accessoires connectés, telles que Project Glass, continueront probablement d'augmenter la densité de pixels.

En pratique, les images à faible densité doivent être les mêmes sur les nouveaux écrans que sur les anciens. Cependant, par rapport aux images nettes à haute densité que les utilisateurs ont l'habitude de voir, les images à faible densité sont floues et pixelisées. Voici une simulation approximative de l'apparence d'une image x1 sur un écran x2. En revanche, l'image x2 est assez bien.

Babouin 1x
Babouin 2x
Des babouins ! à différentes densités de pixels

Pixels sur le Web

Lors de la conception du Web, 99% des écrans avaient une résolution de 96 ppp (ou semblait être), et peu de dispositions ont été prises pour apporter des variantes à cet aspect. En raison des variations importantes des tailles et des densités d'écran, nous avions besoin d'une méthode standard pour que les images s'affichent correctement sur différentes densités et dimensions d'écran.

La spécification HTML a récemment abordé ce problème en définissant un pixel de référence que les fabricants utilisent pour déterminer la taille d'un pixel CSS.

À l'aide du pixel de référence, un fabricant peut déterminer la taille du pixel physique de l'appareil par rapport au pixel standard ou idéal. Ce ratio est appelé "ratio de pixels de l'appareil".

Calcul du rapport de pixels de l'appareil

Supposons qu'un smartphone dispose d'un écran d'une taille physique en pixels de 180 pixels par pouce (ppp). Pour calculer le ratio de pixels de l'appareil, vous devez suivre trois étapes:

  1. Comparez la distance réelle à laquelle l'appareil est tenu à la distance du pixel de référence.

    D'après les spécifications, nous savons qu'avec un format de 28 pouces, la taille idéale est de 96 pixels par pouce. Toutefois, comme il s'agit d'un smartphone, les utilisateurs tiennent l'appareil plus près de leur visage qu'ils tiennent un ordinateur portable. Estimons cette distance à 18 pouces.

  2. Multipliez le rapport de distance par rapport à la densité standard (96 ppp) pour obtenir la densité de pixels idéale pour la distance donnée.

    idéalPixelDensity = (28/18) * 96 = 150 pixels par pouce (environ)

  3. Prenez le rapport entre la densité physique en pixels et la densité de pixels idéale pour obtenir le ratio de pixels de l'appareil.

    devicePixelRatio = 180/150 = 1,2

Méthode de calcul du ratio devicePixelRatio
Schéma illustrant un pixel angulaire de référence pour illustrer le calcul du ratio devicePixelRatio.

Ainsi, lorsqu'un navigateur a besoin de savoir comment redimensionner une image pour l'adapter à la résolution idéale ou standard, le navigateur fait référence au ratio de pixels de l'appareil de 1,2, ce qui signifie que pour chaque pixel idéal, cet appareil dispose de 1,2 pixel physique. La formule pour alterner entre les pixels idéaux (tels que définis par les spécifications Web) et les pixels physiques (points sur l'écran de l'appareil) est la suivante:

physicalPixels = window.devicePixelRatio * idealPixels

Par le passé, les fournisseurs d'appareils avaient tendance à arrondir devicePixelRatios (DPR). Les iPhone et iPad d'Apple indiquent un rapport de DPR de 1, et leurs équivalents avec Retina de 2. La spécification CSS recommande que

l'unité de pixels désigne le nombre entier de pixels d'appareil qui se rapproche le plus du pixel de référence.

L'une des raisons pour lesquelles les ratios arrondis peuvent être meilleurs est qu'ils peuvent réduire la quantité d'artefacts de sous-pixel.

Cependant, la réalité du paysage des appareils est beaucoup plus variée et les téléphones Android ont souvent un DPR de 1, 5. La tablette Nexus 7 a un DPR d'environ 1,33, qui a été obtenu d'après un calcul semblable à celui ci-dessus. Attendez-vous à voir plus d'appareils avec des DPR variables à l'avenir. Pour cette raison, ne partez jamais du principe que vos clients utilisent des DPR entiers.

Présentation des techniques d'image HiDPI

Il existe de nombreuses techniques permettant d'afficher des images de meilleure qualité aussi rapidement que possible, qui appartiennent généralement à deux catégories:

  1. Optimiser des images uniques
  2. Optimisation de la sélection entre plusieurs images

Approches basées sur une seule image: n'utilisez qu'une seule image, mais utilisez-la de manière intelligente. Ces approches ont l'inconvénient de sacrifier inévitablement les performances, car vous téléchargerez des images HiDPI même sur des appareils plus anciens avec un PPP inférieur. Voici quelques approches pour le cas d'une seule image:

  • Image HiDPI fortement compressée
  • Un format d'image génial
  • Format d'image progressif

Approches axées sur plusieurs images: utilisez plusieurs images, mais choisissez de manière intelligente celle à charger. Ces approches entraînent des coûts inhérents à la création de plusieurs versions du même élément, puis à l'élaboration d'une stratégie de décision. Voici les options disponib :

  • JavaScript
  • Distribution côté serveur
  • Requêtes média CSS
  • Fonctionnalités intégrées du navigateur (image-set(), <img srcset>)

Image HiDPI fortement compressée

Pour les images, 60% de la bande passante est déjà consacrée au téléchargement d'un site Web moyen. En diffusant des images HiDPI à tous nos clients, nous augmenterons ce nombre. Dans quelle mesure va-t-elle se développer ?

J'ai effectué des tests qui ont généré des fragments d'image 1 et 2 fois plus élevés avec une qualité JPEG à 90, 50 et 20. Voici le script shell que j'ai utilisé (en utilisant ImageMagick) pour les générer:

Exemple de cartes 1. Exemple de cartes 2. Exemple de cartes 3.
Exemples d'images avec différentes compressions et densités de pixels

D'après ce petit échantillonnage non scientifique, la compression d'images volumineuses semble être un bon compromis entre la qualité et la taille. À mon œil, les images 2x fortement compressées sont en fait mieux que les images x1 non compressées.

Bien entendu, diffuser des images 2x de faible qualité et hautement compressées sur des appareils x2 est moins efficace que de diffuser des images de meilleure qualité, et l'approche ci-dessus entraîne des pénalités de qualité d'image. Si vous comparez la qualité (90 images) à la qualité (20 images), vous constaterez une baisse de netteté et un grain accru. Ces artefacts peuvent ne pas être acceptables dans les cas où des images de haute qualité sont essentielles (par exemple, une application de visualisation de photos) ou pour les développeurs d'applications qui ne sont pas prêts à faire de compromis.

La comparaison ci-dessus a été effectuée entièrement avec des fichiers JPEG compressés. Il convient de noter qu'il existe de nombreux compromis entre les formats d'image couramment utilisés (JPEG, PNG, GIF), ce qui nous amène à...

Un format d'image génial

WebP est un format d'image assez convaincant qui se compresse très bien tout en conservant une image haute fidélité. Bien sûr, il n'est pas encore implémenté partout.

Vous pouvez vérifier la prise en charge de WebP via JavaScript. Vous chargez une image de 1 px via data-uri, attendez le déclenchement d'événements chargés ou d'erreur, puis vérifiez que la taille est correcte. Modernizr est fourni avec un script de détection de fonctionnalités, disponible via Modernizr.webp.

Cependant, il est préférable de le faire directement dans CSS à l'aide de la fonction image(). Ainsi, si vous disposez d'une image WebP et d'une image de remplacement JPEG, vous pouvez écrire ce qui suit:

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

Cette approche pose quelques problèmes. Premièrement, image() n'est pas du tout implémenté. Deuxièmement, bien que la compression WebP élimine le JPEG de l'eau, il s'agit toujours d'une amélioration relativement progressive (environ 30% plus petite d'après cette galerie WebP). Ainsi, WebP seul ne suffit pas à résoudre le problème de PPP élevé.

Formats d'image progressifs

Les formats d'image progressifs tels que JPEG 2000, Progressive JPEG, Progressive PNG et GIF présentent l'avantage (parfois débattu) de voir l'image s'afficher avant son chargement complet. Ils peuvent entraîner une surcharge de taille, mais il existe des preuves contradictoires à ce sujet. Jeff Atwood a affirmé que le mode progressif "ajoute environ 20% à la taille des images PNG et environ 10% à la taille des images JPEG et GIF". Cependant, Stoyan Stefanov a affirmé que le mode progressif est plus efficace (dans la plupart des cas) pour les fichiers volumineux.

À première vue, les images progressives semblent très prometteuses lorsqu'il s'agit de diffuser des images de la meilleure qualité possible le plus rapidement possible. L'idée est que le navigateur peut arrêter de télécharger et de décoder une image une fois qu'il sait que des données supplémentaires n'amélioreront pas la qualité de l'image (c'est-à-dire que toutes les améliorations de la fidélité sont des sous-pixels).

Bien que les connexions soient faciles à interrompre, leur redémarrage est souvent coûteux. Pour un site comportant de nombreuses images, l'approche la plus efficace consiste à maintenir active une seule connexion HTTP et à la réutiliser aussi longtemps que possible. Si la connexion est interrompue prématurément parce qu'une image a été suffisamment téléchargée, le navigateur doit créer une nouvelle connexion, ce qui peut être très lent dans les environnements à faible latence.

Une solution consiste à utiliser la requête HTTP Range (Plage HTTP), qui permet aux navigateurs de spécifier une plage d'octets à récupérer. Un navigateur intelligent peut effectuer une requête HEAD pour accéder à l'en-tête, le traiter, décider de la quantité d'image nécessaire, puis l'extraire. Malheureusement, la plage HTTP est peu compatible avec les serveurs Web, ce qui rend cette approche peu pratique.

Enfin, une limite évidente de cette approche est que vous n'avez pas à choisir l'image à charger. Vous ne pouvez qu'utiliser différentes fidélités pour la même image. Par conséquent, il ne s'agit pas du cas d'utilisation sens artistique.

Utiliser JavaScript pour choisir l'image à charger

La première approche et la plus évidente pour décider quelle image charger consiste à utiliser JavaScript dans le client. Cette approche vous permet de tout savoir sur votre user-agent et de prendre les mesures appropriées. Vous pouvez déterminer le ratio de pixels de l'appareil via window.devicePixelRatio, obtenir la largeur et la hauteur de l'écran, et même éventuellement effectuer un reniflage de connexion réseau via navigateurator.connection ou émettre une requête fictive, comme le fait la bibliothèque foresight.js. Une fois que vous avez collecté toutes ces informations, vous pouvez décider de l'image à charger.

Il existe environ un million de bibliothèques JavaScript qui offrent des fonctionnalités semblables à celles décrites ci-dessus, et malheureusement aucune d'entre elles n'est particulièrement remarquable.

L'un des principaux inconvénients de cette approche est que, si vous utilisez JavaScript, le chargement de l'image est retardé jusqu'à la fin de l'analyseur d'anticipation. Cela signifie essentiellement que le téléchargement des images ne commence même qu'après le déclenchement de l'événement pageload. Pour en savoir plus, consultez l'article de James Grigsby.

Choisir l'image à charger sur le serveur

Vous pouvez reporter la décision côté serveur en écrivant des gestionnaires de requêtes personnalisés pour chaque image que vous diffusez. Un tel gestionnaire vérifiera la compatibilité avec Retina en fonction de l'user-agent (la seule information transmise au serveur). Ensuite, selon que la logique côté serveur souhaite diffuser des éléments HiDPI ou non, vous chargez l'élément approprié (nommé selon une convention connue).

Malheureusement, le user-agent ne fournit pas nécessairement suffisamment d'informations pour décider si un appareil doit recevoir des images de haute ou de mauvaise qualité. De plus, il va sans dire que tout ce qui concerne l'user-agent est un piratage et doit être évité si possible.

Utiliser des requêtes média CSS

Les requêtes média CSS sont déclaratives. Elles vous permettent d'indiquer votre intention et de laisser le navigateur s'occuper de l'action à votre place. En plus de l'utilisation la plus courante des requêtes média (correspondant à la taille de l'appareil), vous pouvez également établir une correspondance avec devicePixelRatio. Comme vous pouvez vous y attendre, la requête média associée est "device-pixel-ratio" et est associée à des variantes minimale et maximale. Si vous souhaitez charger des images à PPP élevé et que le ratio de pixels de l'appareil dépasse un certain seuil, voici ce que vous pouvez faire:

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

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

Cela se complique un peu plus avec tous les préfixes de fournisseur mélangés, en particulier à cause de différences d'emplacement extraordinaires entre les préfixes "min" et "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);
  }
}

Avec cette approche, vous retrouvez les avantages de l'analyse anticipée, qui n'était plus disponible avec la solution JS. Vous gagnez également la flexibilité de choisir vos points d'arrêt réactifs (par exemple, vous pouvez avoir des images PPP faible, moyen et élevé), qui a été perdue avec l'approche côté serveur.

Malheureusement, il est encore un peu gênant et conduit à un CSS d'aspect étrange (ou nécessite un prétraitement). En outre, cette approche est limitée aux propriétés CSS. Il n'est donc pas possible de définir un <img src>, et vos images doivent toutes être des éléments avec un arrière-plan. Enfin, en s'appuyant strictement sur le ratio de pixels de l'appareil, votre smartphone à PPP élevé peut finir par télécharger un élément image x2 massif sur une connexion EDGE. Ce n'est pas la meilleure expérience utilisateur possible.

Utiliser les nouvelles fonctionnalités du navigateur

De nombreuses discussions ont récemment eu lieu sur la prise en charge des plates-formes Web pour le problème d'image PPP élevé. Apple a récemment fait un entrée dans le secteur, en apportant la fonction CSS image-set() à WebKit. Par conséquent, Safari et Chrome sont tous deux compatibles. Comme il s'agit d'une fonction CSS, image-set() ne résout pas le problème lié aux balises <img>. Saisissez @srcset pour résoudre ce problème, mais (au moment de la rédaction de ce document) ne dispose (encore) d'aucune implémentation de référence. La section suivante aborde plus en détail image-set et srcset.

Fonctionnalités du navigateur pour une prise en charge des pixels haute résolution

En fin de compte, le choix de l'approche à adopter dépend de vos besoins particuliers. Cela dit, gardez à l'esprit que toutes les approches mentionnées ci-dessus présentent des inconvénients. Cependant, une fois que image-set et srcset seront largement acceptés, ils constitueront les solutions appropriées à ce problème. Pour le moment, penchons-nous sur quelques bonnes pratiques qui peuvent nous rapprocher le plus possible de cet avenir idéal.

Premièrement, en quoi ces deux éléments sont-ils différents ? Eh bien, image-set() est une fonction CSS pouvant être utilisée comme valeur de la propriété CSS d'arrière-plan. "srcset" est un attribut spécifique aux éléments <img>, avec une syntaxe similaire. Ces deux balises vous permettent de spécifier des déclarations d'image, mais l'attribut srcset vous permet également de configurer l'image à charger en fonction de la taille de la fenêtre d'affichage.

Bonnes pratiques pour les ensembles d'images

La fonction CSS image-set() est disponible avec le préfixe -webkit-image-set(). La syntaxe est assez simple et prend une ou plusieurs déclarations d'image séparées par des virgules, qui se composent d'une chaîne d'URL ou d'une fonction url() suivie de la résolution associée. Exemple :

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

Cela indique au navigateur qu'il existe deux images parmi lesquelles choisir. L'une d'elles est optimisée pour les écrans x1 et l'autre pour les écrans x2. Le navigateur choisit ensuite celui à charger en fonction de divers facteurs, y compris la vitesse du réseau, s'il est suffisamment intelligent (à ma connaissance, actuellement).

En plus de charger la bonne image, le navigateur la met également à l'échelle en conséquence. En d'autres termes, le navigateur suppose que deux images sont deux fois plus grandes que des images 1x, et réduit donc l'image x2 avec un facteur de 2, de sorte que l'image semble avoir la même taille sur la page.

Au lieu de spécifier 1x, 1,5x ou Nx, vous pouvez également spécifier une certaine densité de pixels de l'appareil en dpi.

Cela fonctionne bien, sauf dans les navigateurs qui ne sont pas compatibles avec la propriété image-set, qui n'affiche aucune image. Ce n'est clairement pas une bonne chose. Vous devez donc utiliser une solution de remplacement (ou une série de solutions de remplacement) pour résoudre ce problème:

background-image: url(icon1x.jpg);
background-image: -webkit-image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);
/* This will be useful if image-set gets into the platform, unprefixed.
    Also include other prefixed versions of this */
background-image: image-set(
  url(icon1x.jpg) 1x,
  url(icon2x.jpg) 2x
);

La commande ci-dessus charge l'élément approprié dans les navigateurs compatibles avec "image-set" et revient à l'élément 1x dans le cas contraire. La mise en garde évidente est que même si la compatibilité avec les navigateurs image-set() est faible, la plupart des user-agents obtiendront l'élément 1x.

Cette démonstration utilise image-set() pour charger la bonne image, en revenant à l'élément 1x si cette fonction CSS n'est pas compatible.

À ce stade, vous vous demandez peut-être pourquoi ne pas simplement utiliser un polyfill (c'est-à-dire créer un shim JavaScript pour) image-set() et l'appeler "une journée". Il s'avère qu'il est assez difficile d'implémenter des polyfills efficaces pour les fonctions CSS. (Pour en savoir plus, consultez cette discussion de style www.)

Image srcset

Voici un exemple de "srcset" :

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

Comme vous pouvez le voir, en plus des déclarations x fournies par image-set, l'élément "srcset" accepte également les valeurs "w" et "h" qui correspondent à la taille de la fenêtre d'affichage, afin de tenter de diffuser la version la plus pertinente. L'image ci-dessus diffuse des bannières-phone.jpeg sur les appareils dont la largeur de la fenêtre d'affichage est inférieure à 640 pixels, la bannière-phone-HD.jpeg sur les appareils à petit écran haute résolution, la bannière "banner-HD.jpeg" sur les appareils dont la résolution est supérieure à 640 pixels et la bannière.jpeg sur tout le reste.

Utiliser "image-set" pour les éléments d'image

Étant donné que l'attribut srcset sur les éléments img n'est pas implémenté dans la plupart des navigateurs, il peut être tentant de remplacer vos éléments img par des <div>s avec des arrière-plans et d'utiliser une approche basée sur des images. Cela fonctionnera, avec des mises en garde. L'inconvénient est que la balise <img> a une valeur sémantique de longue durée. En pratique, cela est surtout important pour les robots d'exploration et pour des raisons d'accessibilité.

Si vous finissez par utiliser -webkit-image-set, vous pourriez être tenté d'utiliser la propriété CSS d'arrière-plan. L'inconvénient de cette approche est que vous devez spécifier la taille d'image, ce qui est inconnu si vous utilisez une image qui n'est pas de type 1x. Plutôt que de le faire, vous pouvez utiliser la propriété CSS de contenu comme suit:

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

L'image est mise à l'échelle automatiquement en fonction du paramètre devicePixelRatio. Consultez cet exemple de la technique ci-dessus en action, avec une valeur de remplacement supplémentaire pour url() pour les navigateurs qui ne sont pas compatibles avec image-set.

Polyfilling srcset

L'une des caractéristiques pratiques de srcset est qu'il est fourni avec une création de remplacement naturelle. Dans le cas où l'attribut srcset n'est pas implémenté, tous les navigateurs savent qu'ils doivent traiter l'attribut src. De plus, comme il ne s'agit que d'un attribut HTML, il est possible de créer des polyfills avec JavaScript.

Ce polyfill est fourni avec des tests unitaires pour s'assurer qu'il est aussi proche que possible des spécifications. De plus, des vérifications sont en place pour empêcher le polyfill d'exécuter du code si l'attribut srcset est implémenté de manière native.

Voici une démonstration du polyfill en action.

Conclusion

Il n'existe pas de solution miracle pour résoudre le problème des images PPP élevées.

La solution la plus simple consiste à éviter complètement les images, en optant plutôt pour le format SVG et CSS. Cependant, ce n'est pas toujours réaliste, surtout si votre site comporte des images de haute qualité.

Les approches JavaScript, CSS et l'utilisation côté serveur présentent toutes leurs forces et leurs faiblesses. L'approche la plus prometteuse, cependant, consiste à exploiter les nouvelles fonctionnalités des navigateurs. Bien que la prise en charge de image-set et srcset par les navigateurs ne soit pas encore terminée, il existe des solutions de remplacement raisonnables à utiliser aujourd'hui.

Pour résumer, voici nos recommandations: