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

L'une des caractéristiques du paysage complexe actuel des appareils est qu'il existe une très large gamme de densités de pixels d’écran. Certains appareils sont dotés d'écrans à très haute résolution, tandis que d'autres sont à la traîne. Les développeurs d'applications doivent prendre en charge un éventail de densités de pixels, ce qui peut s'avérer très 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 de batterie limitées.

En termes d'images, l'objectif des développeurs d'applications Web est de fournir les images de la meilleure qualité aussi efficacement que possible. Cet article présente quelques techniques utiles pour y parvenir dès aujourd'hui et dans un avenir proche.

Évitez les images, si possible

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

Cela dit, il n'est pas toujours possible d'éviter les images matricielles. Par exemple, vous pouvez recevoir des éléments qui seraient très difficiles à reproduire en SVG/CSS pur, ou vous avez affaire à une photographie. Bien que vous puissiez convertir automatiquement l'image au format SVG, la vectorisation de photographies n'a guère de sens, car les versions agrandies ne sont généralement pas esthétiques.

Contexte

Bref historique de la densité d'affichage

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

La densité de pixels des écrans s'est progressivement améliorée, en grande partie en raison du cas d'utilisation mobile, dans lequel les utilisateurs tiennent généralement leur téléphone plus près de leur visage, ce qui rend les pixels plus visibles. En 2008, les téléphones 150 dpi sont devenus la nouvelle norme. La tendance à l'augmentation de la densité d'affichage s'est poursuivie, et les nouveaux téléphones actuels affichent des écrans 300 ppp (identifiés par Apple).

Le Saint-Graal, bien sûr, est un écran dans lequel les pixels sont entièrement invisibles. Pour le facteur de forme du téléphone, la génération actuelle d'écrans Retina/HiDPI peut s'approcher de cet idéal. Toutefois, de nouvelles catégories de matériel et d'accessoires connectés, comme le Project Glass, devraient continuer à augmenter la densité de pixels.

En pratique, les images à faible densité devraient être identiques sur les nouveaux écrans que sur les anciens. Toutefois, par rapport aux images nettes que les utilisateurs sont habitués à voir sur les écrans à haute densité, les images à faible densité semblent choquantes et pixellisées. Vous trouverez ci-dessous une simulation approximative de l'apparence d'une image 1x sur un écran 2x. En revanche, l'image 2 x est plutôt réussie.

Babouin 1x
Babouin 2x
Baboons ! à différentes densités de pixels.

Pixels sur le Web

Lorsque le Web a été conçu, 99 % des écrans affichaient 96 dpi (ou prétendaient l'être), et peu de dispositions ont été prises pour les variations à cet égard. En raison des grandes variations de taille et de densité d'écran, nous avions besoin d'une méthode standard pour que les images soient agréables à regarder sur différentes densités et dimensions d'écran.

La spécification HTML a récemment résolu 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 rapport est appelé "rapport de pixels de l'appareil".

Calculer le rapport de pixels de l'appareil

Supposons qu'un smartphone dispose d'un écran dont la taille physique des pixels est de 180 pixels par pouce (ppp). Le calcul du rapport de pixels de l'appareil se fait en trois étapes :

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

    Conformément aux spécifications, nous savons que pour un format de 28 pouces, l'idéal est de 96 pixels par pouce. Cependant, comme il s'agit d'un smartphone, les utilisateurs tiennent l'appareil plus près de leur visage que s'il s'agissait d'un ordinateur portable. Estimons cette distance à 45 cm.

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

    idealPixelDensity = (28/18) * 96 = 150 pixels par pouce (environ)

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

    devicePixelRatio = 180/150 = 1,2

Calcul de devicePixelRatio.
Schéma représentant un pixel angulaire de référence, pour illustrer le calcul de devicePixelRatio.

Ainsi, lorsqu'un navigateur doit savoir comment redimensionner une image pour l'adapter à l'écran en fonction de la résolution idéale ou standard, il se réfère au rapport de pixels de l'appareil de 1,2, ce qui signifie que pour chaque pixel idéal, cet appareil dispose de 1,2 pixels physiques. La formule permettant de passer des pixels idéaux (tels que définis par les spécifications Web) aux pixels physiques (points sur l'écran de l'appareil) est la suivante :

physicalPixels = window.devicePixelRatio * idealPixels

Historiquement, les fournisseurs d'appareils ont tendance à arrondir les devicePixelRatios (DPR). L'iPhone et l'iPad d'Apple affichent un DPR de 1, et leurs équivalents Retina affichent 2. La spécification CSS recommande ce qui suit :

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

L'une des raisons pour lesquelles les rapports d'arrondi peuvent être meilleurs est qu'ils peuvent générer moins d'artefacts de sous-pixel.

Toutefois, 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, obtenu par un calcul semblable à celui ci-dessus. Vous verrez de plus en plus d'appareils avec des DPR variables à l'avenir. Pour cette raison, vous ne devez jamais supposer que vos clients disposent de fichiers DPR entiers.

Présentation des techniques d'image HiDPI

Il existe de nombreuses techniques pour résoudre le problème de l'affichage des images de la meilleure qualité le plus rapidement possible. Elles se divisent en deux catégories principales :

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

Approches à image unique : utilisez une seule image, mais faites-en quelque chose d'intelligent. L'inconvénient de ces approches est que vous sacrifiez inévitablement les performances, car vous téléchargez des images HiDPI même sur des appareils plus anciens avec un DPI inférieur. Voici quelques approches pour le cas d'une seule image :

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

Plusieurs approches d'images : utilisez plusieurs images, mais faites preuve d'ingéniosité pour choisir celles à charger. Ces approches impliquent des coûts inhérents pour le développeur, qui doit créer plusieurs versions du même composant, puis élaborer une stratégie de décision. Les différentes options proposées sont les suivantes :

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

Image HiDPI fortement compressée

Les images représentent déjà 60 % de la bande passante utilisée pour télécharger un site Web moyen. En diffusant des images HiDPI auprès de tous les clients, nous allons augmenter ce nombre. De combien augmentera-t-il ?

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

Mosaïque exemple 1. Exemple de cartes 2. Exemple de cartes 3.
Échantillons d'images avec différentes compressions et densités de pixels.

D'après cet échantillonnage restreint et non scientifique, il semble que la compression des grandes images offre un bon compromis qualité/taille. À mon avis, les images 2x fortement compressées sont en fait plus belles que les images 1x non compressées.

Bien entendu, diffuser des images de qualité inférieure et fortement compressées en double résolution sur des appareils en double résolution est moins efficace que de diffuser des images de meilleure qualité. L'approche ci-dessus entraîne des pénalités de qualité d'image. Si vous comparez des images de qualité : 90 à des images de qualité : 20, vous constaterez une baisse de la netteté et une augmentation du grain. Ces artefacts peuvent ne pas être acceptables dans les cas où les images de haute qualité sont essentielles (par exemple, une application de visionneuse de photos) ou pour les développeurs d'applications qui ne sont pas prêts à faire des compromis.

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

Format d'image totalement génial

WebP est un format d'image attrayant qui compresse très bien les images tout en conservant une fidélité élevée. Bien sûr, il n'est pas encore implémenté partout.

Vous pouvez vérifier la compatibilité avec WebP via JavaScript. Vous chargez une image de 1 px via data-uri, attendez que des événements de chargement ou d'erreur soient déclenchés, puis vérifiez que la taille est correcte. Modernizr est fourni avec un tel script de détection de fonctionnalités, disponible via Modernizr.webp.

Cependant, un meilleur moyen de procéder consiste à utiliser directement le code CSS, à l'aide de la fonction image(). Par conséquent, si vous disposez d'une image WebP et d'un remplacement JPEG, vous pouvez écrire ce qui suit :

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

Cette approche pose un certain nombre de problèmes. Tout d'abord, image() n'est pas du tout largement implémenté. Deuxièmement, bien que la compression WebP surpasse le format JPEG, il s'agit toujours d'une amélioration relativement incrémentielle (environ 30 % de plus petite taille d'après cette galerie WebP). Par conséquent, WebP seul ne suffit pas à résoudre le problème de DPI élevé.

Formats d'image progressifs

Les formats d'images progressifs tels que JPEG 2000, Progressive JPEG, Progressive PNG et GIF présentent l'avantage (parfois discuté) de voir l'image se mettre en place avant qu'elle ne soit entièrement chargée. Elles peuvent entraîner une augmentation de la taille, bien qu'il existe des preuves contradictoires à ce sujet. Jeff Atwood a déclaré que le mode progressif "ajoutait environ 20% à la taille des images PNG et environ 10% à celle des images JPEG et GIF". Toutefois, Stoyan Stefanov a affirmé que pour les fichiers volumineux, le mode progressif est plus efficace (dans la plupart des cas).

À première vue, les images progressives semblent très prometteuses pour diffuser des images de la meilleure qualité aussi rapidement que 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 les données supplémentaires n'amélioreront pas la qualité de l'image (c'est-à-dire que toutes les améliorations de fidélité sont inférieures au pixel).

Bien que les connexions soient faciles à interrompre, leur redémarrage est souvent coûteux. Pour un site contenant de nombreuses images, l'approche la plus efficace consiste à maintenir une seule connexion HTTP active, en la réutilisant 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 alors créer une nouvelle connexion, ce qui peut être très lent dans les environnements à faible latence.

Pour contourner ce problème, vous pouvez utiliser la requête HTTP Range, qui permet aux navigateurs de spécifier une plage d'octets à extraire. Un navigateur intelligent peut envoyer une requête HEAD pour accéder à l'en-tête, le traiter, déterminer la quantité d'image réellement nécessaire, puis extraire les données. Malheureusement, la plage HTTP n'est pas bien prise en charge par les serveurs Web, ce qui rend cette approche peu pratique.

Enfin, une limite évidente de cette approche est que vous ne pouvez pas choisir l'image à charger, mais seulement les fidélités différentes de la même image. Par conséquent, cela ne répond pas au cas d'utilisation de la direction artistique.

Déterminer quelle image charger à l'aide de JavaScript

La première approche, et la plus évidente, pour choisir l'image à charger consiste à utiliser JavaScript dans le client. Cette approche vous permet de tout savoir sur votre user-agent et de faire les bons choix. Vous pouvez déterminer le rapport de pixels de l'appareil via window.devicePixelRatio, obtenir la largeur et la hauteur de l'écran, et même détecter une connexion réseau via navigateur.connection ou émettre une fausse requête, comme le fait la bibliothèque foresight.js. Une fois que vous avez collecté toutes ces informations, vous pouvez choisir l'image à charger.

Environ 1 million de bibliothèques JavaScript fonctionnent comme celles ci-dessus, et malheureusement aucune d'entre elles n'est particulièrement exceptionnelle.

L'un des principaux inconvénients de cette approche est que l'utilisation de JavaScript retarde le chargement des images jusqu'à la fin de l'analyseur d'anticipation. Cela signifie essentiellement que le téléchargement des images ne commencera même pas tant que l'événement pageload ne se déclenchera pas. Pour en savoir plus, consultez cet article de Jason Grigsby.

Décider de l'image à charger sur le serveur

Vous pouvez différer 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érifie la compatibilité de 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, vous chargez l'élément approprié (nommé selon une convention connue).

Malheureusement, l'User-Agent ne fournit pas nécessairement suffisamment d'informations pour déterminer si un appareil doit recevoir des images de haute ou de basse qualité. De plus, il va sans dire que tout ce qui concerne User-Agent est un piratage et doit être évité dans la mesure du possible.

Utiliser des requêtes média CSS

Étant déclaratives, les requêtes multimédias CSS vous permettent d'indiquer votre intention et de laisser le navigateur faire ce qu'il faut en votre nom. En plus de l'utilisation la plus courante des requêtes média (correspondance à la taille de l'appareil), vous pouvez également établir une correspondance avec devicePixelRatio. La requête multimédia associée est "device-pixel-ratio" et comporte des variantes minimales et maximales, comme vous pouvez vous y attendre. 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 lorsque tous les préfixes de fournisseurs sont mélangés, en particulier à cause de différences d'emplacement incroyables 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 ont été perdus avec la solution JS. Vous bénéficiez également de la flexibilité de choisir vos points d'arrêt responsifs (par exemple, vous pouvez avoir des images à faible, moyenne et haute résolution), ce qui était perdu avec l'approche côté serveur.

Malheureusement, il est encore un peu lourd et donne un CSS d'apparence étrange (ou nécessite un prétraitement). En outre, cette approche est limitée aux propriétés CSS. Il n'y a donc aucun moyen de définir un <img src>, et vos images doivent toutes être des éléments avec un arrière-plan. Enfin, en vous appuyant strictement sur le format de pixel de l'appareil, vous pouvez vous retrouver dans des situations où votre smartphone haute résolution finit par télécharger un élément image massif 2 x sur une connexion EDGE. Ce n'est pas la meilleure expérience utilisateur.

Utiliser les nouvelles fonctionnalités du navigateur

De nombreuses discussions ont récemment été relatives à la compatibilité des plates-formes Web avec le problème des images haute résolution. Apple a récemment fait son entrée dans ce domaine en apportant la fonction CSS image-set() à WebKit. Elle est donc compatible avec Safari et Chrome. Étant donné qu'il s'agit d'une fonction CSS, image-set() ne résout pas le problème pour les balises <img>. Utilisez @srcset, qui résout ce problème, mais (au moment de la rédaction de cet article) n'a pas encore d'implémentations de référence. La section suivante décrit plus en détail image-set et srcset.

Fonctionnalités du navigateur pour la compatibilité avec les écrans haute résolution

En fin de compte, la décision concernant l'approche que vous allez adopter dépend de vos besoins spécifiques. Cela dit, gardez à l'esprit que toutes les approches mentionnées ci-dessus présentent des inconvénients. À l'avenir, cependant, une fois que image-set et srcset seront largement compatibles, ils constitueront les solutions appropriées à ce problème. Pour l'instant, parlons de quelques bonnes pratiques qui peuvent nous rapprocher le plus possible de cet avenir idéal.

Tout d'abord, en quoi ces deux éléments sont-ils différents ? image-set() est une fonction CSS, qui peut ê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'images, 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 concernant les ensembles d'images

La fonction CSS image-set() est disponible avec le préfixe -webkit-image-set(). La syntaxe est assez simple. Elle prend une ou plusieurs déclarations d'images séparées par une virgule, qui consistent en une chaîne d'URL ou en 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 a le choix entre deux images. L'un est optimisé pour les écrans 1x, et l'autre pour les écrans 2x. Le navigateur peut ensuite choisir lequel charger en fonction de divers facteurs, y compris la vitesse du réseau, si le navigateur est suffisamment intelligent (pas encore implémenté à ma connaissance).

En plus de charger l'image appropriée, 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 les images x1. Il réduit donc l'image x2 selon un facteur de 2, de sorte que l'image ait 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.

Cette méthode fonctionne bien, sauf dans les navigateurs qui ne prennent pas en charge la propriété image-set, qui n'affiche aucune image. Ce n'est clairement pas une bonne chose. Vous devez donc utiliser un fallback (ou une série de fallbacks) 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
);

Ce qui précède charge l'élément approprié dans les navigateurs compatibles avec la définition d'images. Sinon, l'élément 1x est renvoyé à l'élément 1x. L'inconvénient évident est que, bien que la compatibilité avec le navigateur image-set() soit faible, la plupart des agents utilisateur recevront l'élément 1x.

Cette démonstration utilise image-set() pour charger l'image appropriée, et utilise l'élément 1x si cette fonction CSS n'est pas prise en charge.

À ce stade, vous vous demandez peut-être pourquoi ne pas simplement créer un polyfill (c'est-à-dire créer un shim JavaScript) image-set() et l'appeler "journée" ? Il s'avère qu'il est assez difficile d'implémenter des polyfills efficaces pour les fonctions CSS. (Pour une explication détaillée, consultez cette discussion sur le 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 prend également des valeurs w et h qui correspondent à la taille de la fenêtre d'affichage, en essayant de diffuser la version la plus pertinente. La bannière banner-phone.jpeg est diffusée sur les appareils dont la largeur de la vue d'ensemble est inférieure à 640 pixels, banner-phone-HD.jpeg sur les appareils à petit écran et haute résolution, banner-HD.jpeg sur les appareils à haute résolution dont l'écran est supérieur à 640 pixels et banner.jpeg sur tous les autres appareils.

Utiliser image-set pour les éléments 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 l'approche basée sur l'ensemble d'images. Cela fonctionne, mais avec des réserves. L'inconvénient est que la balise <img> a une valeur sémantique à long terme. En pratique, cela est important, surtout 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 "background". L'inconvénient de cette approche est que vous devez spécifier la taille de l'image, qui est inconnue si vous utilisez une image autre que 1x. Au lieu de cela, 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 alors mise à l'échelle automatiquement en fonction de devicePixelRatio. Consultez cet exemple de la technique ci-dessus en action, avec un remplacement supplémentaire par url() pour les navigateurs qui ne prennent pas en charge image-set.

Polyfill du srcset

srcset offre une fonctionnalité de remplacement naturelle. Dans le cas où l'attribut srcset n'est pas implémenté, tous les navigateurs savent qu'il doit être traité. 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 de la spécification. De plus, des vérifications sont en place pour empêcher le polyfill d'exécuter du code si le srcset est implémenté en mode natif.

Voici une démonstration du polyfill en action.

Conclusion

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

La solution la plus simple consiste à éviter complètement les images et à opter pour le SVG et le CSS. Toutefois, ce n'est pas toujours réaliste, en particulier si votre site contient des images de haute qualité.

Les approches en JS et CSS, ainsi que l'utilisation côté serveur, ont toutes leurs avantages et leurs faiblesses. L'approche la plus prometteuse, cependant, consiste à exploiter les nouvelles fonctionnalités des navigateurs. Bien que la compatibilité des navigateurs avec image-set et srcset soit encore incomplète, il existe des solutions de remplacement raisonnables à utiliser dès aujourd'hui.

Pour résumer, voici mes recommandations :

  • Pour les images de fond, utilisez image-set avec les solutions de remplacement appropriées pour les navigateurs qui ne le prennent pas en charge.
  • Pour les images de contenu, utilisez un polyfill srcset ou utilisez un ensemble d'images (voir ci-dessus).
  • Si vous êtes prêt à sacrifier la qualité de l'image, envisagez d'utiliser des images 2 fois compressées.