Aller au contenu principal
divagations - Retour à l'accueil

Implémentation avancée des images responsives : Travail préparatoire

Guillaume Barbier

Temps de lecture : ~ 11 minutes

Suite au résultat moyennement satisfaisant obtenu hier avec la "moulinette magique" du plugin "Image"(S'ouvre dans un nouvelle fenêtre) d'Eleventy (voir l'entrée de journal de la veille), j'ai décidé de revisiter les images responsives avec une autre méthode d’intégration proposé par le plugin : la création d'un shortcode

Concept et objectifs

L'idée est de privilégier les intégrations explicites d'images responsives à une intégration automatique et potentiellement inappropriée.

Pour ça je vais créer un shortcode que je pourrais ensuite insérer dans n'importe quel contenu généré par Eleventy (HTML, Markdown, Liquid, etc.)

Avec ce "composant" (car au final les shortcodes sont des sortes de composants), j’ambitionne de corriger/ajouter les points suivants :

  • Clarifier le balisage (isoler les srcset dans un des balises <source> pour faciliter la lecture – humaine – de la balise <img>)
  • Utiliser la rendition à 720px de large comme image par défaut (pour les navigateurs qui étrangement ne supporteraient pas les chaînes candidates d'image[1])
  • M'assurer que l'image ne soit pas déformée, même si le CSS ne se charge pas ou est désactivé
  • Garantir l'accessibilité des images
  • Permettre l'insertion d'une légende

Isoler le code : faire un plugin

Un shortcode, c'est potentiellement beaucoup de code. Donc pour éviter que mon fichier de configuration ne devienne une casserole de spaghetti[2], il est préférable que je l'isole.

Mon intuition me dit que ce n'est pas le dernier shortcode que je crée, donc j'anticipe et je créé un dossier /shortcodes/ où je vais pouvoir réunir tous mes fichiers JS (j'envisage de suivre la règle de "un shortcode = un fichier JS")

Une fois le fichier créé selon le format habituel (avec l'export d'une fonction acceptant comme argument un objet eleventyConfig), il ne me reste plus qu'à l'appeler sous forme de plugin (un peu comme je l'ai fait pour les filtres).

// In /shortcodes/responsive-img.js
export default function(eleventyConfig) {
  // the code for the shortcode…
}

// In /eleventy.config.js
import pluginResponsiveImg from "./shortcodes/responsive-img.js";

export default function(eleventyConfig) {
  eleventyConfig.addPlugin(pluginResponsiveImg);
}

Recherches

Maintenant que cette base est posée, avant de passer à l'écriture du code, un peu de recherche s'impose, afin d'être clair sur le code que je vais devoir définir et établir les informations dont j'ai besoin :

Réviser mon référentiel d'appareils pour rester pertinent

Avec toutes ces recherches, il a fallut que je me penche d'un peu plus près sur mes images responsives et tester un peu, et là… patatra ! Je me suis rendu compte que dans la très grande majorité des cas avec des appareils réels (ou simulés à partir des dimensions d'appareils réels), seule ma rendition max (1440 pixels) était appelée. Les plus petites étaient simplement ignorées.

Avertissement

Absence de statistiques complètes et fiables

Désolé, j'ai eu beau chercher[3], je n'ai pas réussi à trouver des statistiques qui prennent en compte la densité des écrans en plus de leur résolution. (Ce qui a son importance pour la suite de la réflexion.)

Donc à la place je me suis simplement basé sur mes propres appareils et ceux listés dans les services d'émulation, complétés avec les statistiques de résolution mobiles de statcounter GlobalStats(Opens in a new window) (sans informations sur la densité)

En me basant sur cet échantillon très limité, j'ai l'impression qu'il est aujourd'hui assez rare de trouver des écrans non-retina[4] de moins de 360 pixels de large. Si je ne suis pas trop à côté de la plaque, cela voudrait dire que la plupart des appareils en circulation attendent (pour les images en plein écran) des images supérieures à 720 pixels (360x2=720)

Après un rapide tour dans Excel avec un petite liste de dimensions d'appareils piochée dans l'émulateur de Chrome (plus quelques valeurs théoriques conservées comme référence), je suis arrivé aux conclusions suivantes :

  • Même si elles sont peu utilisées, je vais continuer à produire et servir les dimensions à 320 et 640 pixels (n'ayant pas trouvé de réponses définitives sur les usages, je ne veux pas exclure par une décision trop radicale)
  • Il me faut une rendition intermédiaire entre 720 et 1440 pixels, pour les gros smartphones "retina". J'opte pour une rendition à 960 pixels de large
  • Il faut que j'essaye d'affiner ma définition de l'attribut sizes (voir plus loin), parce que mes images ne sont jamais affichées bords à bords : Les quelques pixels nécessaires en moins, doublés sur un écran retina, peuvent faire une belle différence.

Inventaire des données nécessaires

Pour mes images responsive, il me faut :

  • L'emplacement de l'image
  • Son (éventuel) texte alt
  • La liste des renditions voulues : largeur et format
  • Le détails des renditions générées (ça le plugin s'en charge)
    Note : Ce qui m’intéresse plus particulièrement, c'est les instructions à placer dans l'attribut srcset (emplacement + l'indication de taille réelle d'image), que l'API nous fourni prêt-à-l'emploi
  • Les infos de l'image par défaut que je veux intégrer :
    • son format (là c'est moi qui décide, ce sera jpeg, bien entendu)
    • sa largeur (là je choisi 720 pixels, car c'est la taille d'affichage max d'un visuel dans un article sur un ordinateur avec un écran non-retina)
    • sa hauteur (là il va falloir que je me base sur ce que me renvoi l'API pour une image de 720px)
    • son URL (là encore, je vais devoir compter sur l'URL[5] fournie par l'API)
  • la valeur de l'attribut sizes, et ça c'est tout une histoire ! (Que je détaille plus loin)

L'attribut sizes

D’après mes lectures, un attribut sizes est nécessaire sur les balises portant une liste de sources (le fameux srcset). Cet attribut sert à indiquer au navigateur quelle est la taille de l'image à l'écran (est-ce qu'elle occupe 50%, 100% de l'écran ? ou 120px seulement ? etc.)

D'après ce que je comprends en lisant les specs de l'attribut sizes(S'ouvre dans un nouvelle fenêtre), il y a (en gros) trois façon de le paramétrer :

  • Utiliser la valeur auto
  • Faire une ou des mediaqueries et lui assigner des valeurs en fonction
  • Lui donner une unique valeur, genre 100% ou 100vw

Astuce

Avec sizes, Pas besoin de me soucier de la densité des écrans

L'une des forces de sizes avec les descripteurs de dimensions (par rapport à media avec des descripteurs de densité), c'est que je n'ai absolument pas besoin de me soucier de la densité des écrans :

  1. Avec srcset, j'indique les dimensions de mes sources
  2. Avec sizes, j'indique celle de l'image à afficher
  3. Le navigateur prend ces informations et se charge de choisir la source optimale en fonction de la densité de l'écran

Utiliser la valeur auto ?

La valeur auto est tentante : c'est simple et rapide… en apparence ! Parce qu'en fait il y a des subtilités :

Bref, la valeur auto n'est pas envisageable : je vais devoir trimer un peu…

Définir une valeur "universelle" ?

Parce que je reste malgré tout adepte de la fainéantise (quand elle me donne l'impression d'être malin), je tente de définir une valeur fluide qui s'appliquera à tous les points de bascule (la traduction française de breakpoint). C'est faisable car la disposition de mes pages est simple :

  • Jusqu'à 768px, la largeur de mes images est fluide et correspond à la largeur de l'écran moins 48px (la largeur des deux "marges de sécurité" entre le contenu et le bord de l'écran)
  • Ensuite la largeur du contenu et des images est bloqué à 720px

Je n'utilise pas 100vw pour essayer de coller à la taille réelle des images (qui est moindre du fait des marges de sécurité). Donc j'essaye 90vw qui donne des valeurs assez proche de celle que je pourrais calculer en soustrayant les marges sur la plupart des smartphones.

Mais c'est imparfait :

  • D'après mes analyses dans Excel (j'ai fait quelques calculs avec des résolutions clefs), cette approche fait tout de même souvent télécharger des images plus grandes que si j'avais précisément calculé la largeur
  • Sur de larges écrans non-retina, je me retrouve à charger l'image en 1440px, inutilement grande dans ce contexte (ce n'est pas un problème avec les écrans retina car je ne propose pas d'images supérieurs à 1440px)

Bref, cette approche n'est pas satisfaisante

Cibler précisément les points de bascule ?

OK. On va tenter d'être extrêmement précis pour optimiser au mieux les images chargées. Pour ça, je vais aller chercher les points de bascule exacts où la résolution de l'image chargée devient insuffisante. Le calcul n'est pas trop difficile, chaque point de bascule est égal à :
largeur de l'image devenant insuffisante+48px (les marges de sécurité)+1

Astuce

Pourquoi j'ajoute 1 à la fin de mon calcul ?

Je fais cet ajout car min-width correspond à "est supérieur ou égal à". Il faut donc que je lui donne la valeur immédiatement supérieure à la dernière résolution où ma rendition est suffisante.

Ça me donne le code suivant :

<source srcset="" sizes="
  (min-width: 369px) 640px,
  (min-width: 689px) 720px,
  320px"
>

Astuce

320px comme valeur par défaut

En plaçant 320px à la fin, elle devient la valeur par défaut s'appliquant si aucune mediaquery n'est validée. (Dans ce cas précis, toutes les résolutions strictement inférieures à 369px)

C'est précis, le résultat est optimal, je pourrais m'arrêter là…

Mais c'est un peu verbeux et je me dis que si je fais évoluer mes renditions ou mes points de bascule, ça va vite être fastidieux…

Et si je laissais simplement le navigateur calculer ?

En faisant mes recherches, je suis tombé sur un exemple où la fonction calc() était utilisée dans un attribut sizes… visiblement ça marche !

Ça ravive mon idée première d'une valeur "universelle". Il suffit de laisser la navigateur la calculer :

<source srcset="" sizes="calc(100vw - 48px)">

Et le tour est joué !

Bon… j'introduis quand même une petite subtilité (même si ce n'est pas vraiment nécessaire), c'est plus pour me satisfaire en respectant la logique de la disposition limitée à 720px de large :

<source srcset="" sizes="(max-width: 768px) calc(100vw - 48px), 720px">

Astuce

Explications de ce raffinement superflu

Je limite ma valeur calculée aux résolution inférieures ou égales à 768px. Au delà de cette résolution, la valeur calculée serait supérieure à la largeur maximale d'affichage, je bascule donc sur une largeur fixe de 720px.

J'ai tous les éléments théorique nécessaires pour écrire mon shortcode, mais il se fait tard… ce sera pour demain


  1. "chaîne candidate d'image" sont les descriptions de sources que l'on peut insérer dans un attribut srcset(S'ouvre dans un nouvelle fenêtre) ↩︎

  2. "Code spaghetti", vous l'avez ? ↩︎

  3. Mais peut-être ai-je mal cherché ? Je ne suis pas expert SEO ou Data analyst ↩︎

  4. Petit abus de langage d'un vieil utilisateur iOS. "Retina" est en fait le terme commercial utilisé par Apple pour parler de ses écrans avec une forte densité de pixels (le double d'un écran "normal").

    Donc en théorie on ne devrait pas utiliser ce terme pour les autres marques et plutôt parler d'écrans à haute densité de pixels pour englober les écrans Retina d'Apple et les écrans équivalents de ses concurrents… c'est long ! Et ce n'est pas plus court si je souhaite parler des écrans "normaux" : écrans à faible densité de pixels 😞

    Bref ! J'utiliserais donc retina et non-retina pour parler de tous ces écrans à forte et faible densité de pixels. ↩︎

  5. En vrai, je pourrais la déduire moi-même vu que je vais imposer une nomenclature à l'API. Mais vu que cette opération sera de toute manière faite, autant réutiliser l'existant. ↩︎

  6. MDN indique que la valeur auto n'est pas supporté sur Safari et Firefox avec la balise <img>. Mais rien n'est précisé nul part pour son support avec la balise <source>.

    Je ne sais pas si cela signifie que l'absence de support se cantonne aux cas où une image est intégrée seule avec ses propres attributs srcset et sizes ? Ou si c'est juste un déficit d'info dans la doc MDN de la balise <source> ? ↩︎