Automatiser la sélection des ressources avec des indications client

Ilya Grigorik
Ilya Grigorik

Créer pour le Web vous offre une couverture sans égal. Votre application Web est disponible en un clic sur presque tous les appareils connectés (smartphone, tablette, ordinateur portable et de bureau, téléviseur, etc.), quelle que soit la marque ou la plate-forme. Pour offrir la meilleure expérience possible, vous avez créé un site responsif qui adapte la présentation et les fonctionnalités à chaque facteur de forme. Vous vérifiez maintenant votre checklist de performances pour vous assurer que l'application se charge le plus rapidement possible: vous avez optimisé votre chemin de rendu critique, vous avez compressé et mis en cache vos ressources textuelles, et vous examinez maintenant vos ressources image, qui représentent souvent la majorité des octets transférés. Le problème est que l'optimisation des images est difficile:

  • Déterminer le format approprié (vectoriel ou raster)
  • Déterminer les formats d'encodage optimaux (JPEG, WebP, etc.)
  • Déterminer les paramètres de compression appropriés (avec perte ou sans perte)
  • Déterminer les métadonnées à conserver ou à supprimer
  • Créez plusieurs variantes de chaque élément pour chaque écran et chaque résolution DPR.
  • Tenir compte du type, de la vitesse et des préférences du réseau de l'utilisateur

Chacun de ces problèmes est bien compris. Ensemble, ils créent un vaste espace d'optimisation que nous (les développeurs) négligeons ou ignorons souvent. Les humains ne sont pas très doués pour explorer de manière répétitive le même espace de recherche, en particulier lorsqu'il comporte de nombreuses étapes. En revanche, les ordinateurs excellent dans ce type de tâches.

La réponse à une bonne stratégie d'optimisation durable pour les images et d'autres ressources aux propriétés similaires est simple: l'automatisation. Si vous ajustez manuellement vos ressources, vous faites fausse route: vous les oublierez, vous serez paresseux ou quelqu'un d'autre fera ces erreurs à votre place, c'est garanti.

La saga du développeur soucieux des performances

La recherche dans l'espace d'optimisation des images comporte deux phases distinctes: le temps de compilation et le temps d'exécution.

  • Certaines optimisations sont intrinsèques à la ressource elle-même (par exemple, sélectionner le format et le type d'encodage appropriés, ajuster les paramètres de compression pour chaque encodeur, supprimer les métadonnées inutiles, etc.). Ces étapes peuvent être effectuées au moment de la compilation.
  • D'autres optimisations sont déterminées par le type et les propriétés du client qui les demande et doivent être effectuées au moment de l'exécution : sélection de la ressource appropriée pour la DPR et la largeur d'affichage prévue du client, prise en compte de la vitesse du réseau du client, des préférences de l'utilisateur et de l'application, etc.

Les outils de compilation existent, mais ils pourraient être améliorés. Par exemple, vous pouvez ajuster dynamiquement le paramètre "qualité" pour chaque image et chaque format d'image, mais je n'ai encore jamais vu personne l'utiliser en dehors de la recherche. C'est un domaine propice à l'innovation, mais pour les besoins de cet article, je vais m'en tenir là. Concentrons-nous sur la partie d'exécution de l'histoire.

<img src="/image/thing" sizes="50vw"
        alt="image thing displayed at 50% of viewport width">

L'intent de l'application est très simple: récupérer et afficher l'image à 50% de la vue de l'utilisateur. C'est là que la plupart des concepteurs se lavent les mains et se dirigent vers le bar. Pendant ce temps, le développeur soucieux des performances de l'équipe va passer une longue nuit:

  1. Pour obtenir la meilleure compression possible, elle souhaite utiliser le format d'image optimal pour chaque client: WebP pour Chrome, JPEG XR pour Edge et JPEG pour les autres.
  2. Pour obtenir la meilleure qualité visuelle possible, elle doit générer plusieurs variantes de chaque image à différentes résolutions: 1x, 1,5x, 2x, 2,5x, 3x et peut-être même quelques autres.
  3. Pour éviter de diffuser des pixels inutiles, elle doit comprendre ce que signifie réellement "50% de la fenêtre d'affichage de l'utilisateur". Il existe en effet de nombreuses largeurs de fenêtre d'affichage différentes.
  4. Dans l'idéal, elle souhaite également offrir une expérience résiliente, où les utilisateurs sur des réseaux plus lents récupèrent automatiquement une résolution plus basse. Après tout, il est temps de passer à la verrerie.
  5. L'application expose également certains éléments de contrôle utilisateur qui affectent la ressource d'image à extraire. Il faut donc en tenir compte également.

La conceptrice réalise ensuite qu'elle doit afficher une autre image à 100% de la largeur si la taille de la fenêtre d'affichage est petite pour optimiser la lisibilité. Cela signifie que nous devons maintenant répéter le même processus pour un autre composant, puis rendre la récupération conditionnelle en fonction de la taille de la fenêtre d'affichage. Ai-je déjà mentionné que ce n'est pas facile ? Très bien, allons-y. L'élément picture nous emmènera très loin:

<picture>
    <!-- serve WebP to Chrome and Opera -->
    <source
    media="(min-width: 50em)"
    sizes="50vw"
    srcset="/image/thing-200.webp 200w, /image/thing-400.webp 400w,
        /image/thing-800.webp 800w, /image/thing-1200.webp 1200w,
        /image/thing-1600.webp 1600w, /image/thing-2000.webp 2000w"
    type="image/webp">
    <source
    sizes="(min-width: 30em) 100vw"
    srcset="/image/thing-crop-200.webp 200w, /image/thing-crop-400.webp 400w,
        /image/thing-crop-800.webp 800w, /image/thing-crop-1200.webp 1200w,
        /image/thing-crop-1600.webp 1600w, /image/thing-crop-2000.webp 2000w"
    type="image/webp">
    <!-- serve JPEGXR to Edge -->
    <source
    media="(min-width: 50em)"
    sizes="50vw"
    srcset="/image/thing-200.jpgxr 200w, /image/thing-400.jpgxr 400w,
        /image/thing-800.jpgxr 800w, /image/thing-1200.jpgxr 1200w,
        /image/thing-1600.jpgxr 1600w, /image/thing-2000.jpgxr 2000w"
    type="image/vnd.ms-photo">
    <source
    sizes="(min-width: 30em) 100vw"
    srcset="/image/thing-crop-200.jpgxr 200w, /image/thing-crop-400.jpgxr 400w,
        /image/thing-crop-800.jpgxr 800w, /image/thing-crop-1200.jpgxr 1200w,
        /image/thing-crop-1600.jpgxr 1600w, /image/thing-crop-2000.jpgxr 2000w"
    type="image/vnd.ms-photo">
    <!-- serve JPEG to others -->
    <source
    media="(min-width: 50em)"
    sizes="50vw"
    srcset="/image/thing-200.jpg 200w, /image/thing-400.jpg 400w,
        /image/thing-800.jpg 800w, /image/thing-1200.jpg 1200w,
        /image/thing-1600.jpg 1600w, /image/thing-2000.jpg 2000w">
    <source
    sizes="(min-width: 30em) 100vw"
    srcset="/image/thing-crop-200.jpg 200w, /image/thing-crop-400.jpg 400w,
        /image/thing-crop-800.jpg 800w, /image/thing-crop-1200.jpg 1200w,
        /image/thing-crop-1600.jpg 1600w, /image/thing-crop-2000.jpg 2000w">
    <!-- fallback for browsers that don't support picture -->
    <img src="/image/thing.jpg" width="50%">
</picture>

Nous avons géré la direction artistique, la sélection de formats et fourni six variantes de chaque image pour tenir compte de la variabilité de la DPR et de la largeur de la fenêtre d'affichage de l'appareil du client. Impressionnant !

Malheureusement, l'élément picture ne nous permet pas de définir de règles sur son comportement en fonction du type ou de la vitesse de connexion du client. Toutefois, dans certains cas, son algorithme de traitement permet à l'agent utilisateur d'ajuster la ressource qu'il extrait (voir étape 5). Nous devrons simplement espérer que l'user-agent est suffisamment intelligent. (Remarque: aucune des implémentations actuelles ne l'est). De même, il n'y a pas de crochets dans l'élément picture pour permettre une logique spécifique à l'application qui tient compte des préférences de l'application ou de l'utilisateur. Pour obtenir ces deux derniers bits, nous devrions déplacer toute la logique ci-dessus vers JavaScript, mais cela annule les optimisations du scanner de précharge proposées par picture. Hmm.

En dehors de ces limites, le processus fonctionne. Du moins pour cet élément en particulier. Le véritable défi à long terme est que nous ne pouvons pas nous attendre à ce que le concepteur ou le développeur crée manuellement un code comme celui-ci pour chaque élément. C'est un casse-tête amusant au premier essai, mais il perd immédiatement son attrait. Nous avons besoin d'automatisation. Peut-être que l'IDE ou d'autres outils de transformation de contenu peuvent nous aider et générer automatiquement le modèle ci-dessus.

Automatiser la sélection de ressources avec des indices client

Prenez une profonde inspiration, suspendez votre incrédulité et réfléchissez à l'exemple suivant:

<meta http-equiv="Accept-CH" content="DPR, Viewport-Width, Width">
...
<picture>
    <source media="(min-width: 50em)" sizes="50vw" srcset="/image/thing">
    <img sizes="100vw" src="/image/thing-crop">
</picture>

Croyez-le ou non, l'exemple ci-dessus est suffisant pour fournir toutes les mêmes fonctionnalités que la balise d'image beaucoup plus longue ci-dessus. De plus, comme nous le verrons, il permet au développeur de contrôler entièrement la façon, les ressources et le moment où les ressources d'image sont extraites. La "magie" se trouve dans la première ligne qui active les rapports sur les indices client et indique au navigateur de diffuser au serveur le format de pixel de l'appareil (DPR), la largeur de la vue du navigateur (Viewport-Width) et la largeur d'affichage prévue (Width) des ressources.

Lorsque les indices client sont activés, la balise côté client qui en résulte ne conserve que les exigences de présentation. Le concepteur n'a pas à se soucier des types d'images, des résolutions client, des points de rupture optimaux pour réduire les octets transmis ni d'autres critères de sélection des ressources. Avouons-le, ils ne l'ont jamais fait, et ils ne devraient pas avoir à le faire. Mieux encore, le développeur n'a pas besoin de réécrire et d'étendre le balisage ci-dessus, car la sélection des ressources réelles est négociée par le client et le serveur.

Chrome 46 est compatible en mode natif avec les hints DPR, Width et Viewport-Width. Les indices sont désactivés par défaut, et <meta http-equiv="Accept-CH" content="..."> ci-dessus sert de signal d'activation qui indique à Chrome d'ajouter les en-têtes spécifiés aux requêtes sortantes. Maintenant, examinons les en-têtes de requête et de réponse d'un exemple de requête d'image:

Schéma de la négociation des indices client

Chrome annonce la prise en charge du format WebP via l'en-tête de requête Accept. Le nouveau navigateur Edge annonce également la prise en charge de JPEG XR via l'en-tête Accept.

Les trois en-têtes de requête suivants sont les en-têtes client-hint qui annoncent le format de pixel de l'appareil du client (3x), la largeur de la fenêtre d'affichage de la mise en page (460 px) et la largeur d'affichage prévue de la ressource (230 px). Cela fournit toutes les informations nécessaires au serveur pour sélectionner la variante d'image optimale en fonction de son propre ensemble de règles: disponibilité des ressources prégénérées, coût du réencodage ou du redimensionnement d'une ressource, popularité d'une ressource, charge actuelle du serveur, etc. Dans ce cas particulier, le serveur utilise les indices DPR et Width, et renvoie une ressource WebP, comme indiqué par les en-têtes Content-Type, Content-DPR et Vary.

Il n'y a pas de magie ici. Nous avons déplacé la sélection de ressources de la balise HTML vers la négociation de requête-réponse entre le client et le serveur. Par conséquent, le code HTML ne concerne que les exigences de présentation et peut être écrit par n'importe quel concepteur et développeur, tandis que la recherche dans l'espace d'optimisation des images est déléguée aux ordinateurs et est désormais facilement automatisée à grande échelle. Vous vous souvenez de notre développeur soucieux des performances ? Son travail consiste maintenant à écrire un service d'image qui peut exploiter les indices fournis et renvoyer la réponse appropriée. Elle peut utiliser n'importe quelle langue ou n'importe quel serveur de son choix, ou laisser un service tiers ou un CDN le faire en son nom.

<img src="/image/thing" sizes="50vw"
        alt="image thing displayed at 50% of viewport width">

Vous vous souvenez de ce type ci-dessus ? Avec les hints client, la simple balise d'image est désormais compatible avec la DPR, la largeur et la largeur de la fenêtre d'affichage, sans balisage supplémentaire. Si vous devez ajouter une direction artistique, vous pouvez utiliser la balise picture, comme illustré ci-dessus. Sinon, toutes vos balises d'image existantes sont désormais beaucoup plus intelligentes. Les indices client améliorent les éléments img et picture existants.

Contrôler la sélection des ressources avec un service worker

Un serviceWorker est en fait un proxy côté client exécuté dans votre navigateur. Il intercepte toutes les requêtes sortantes et vous permet d'inspecter, de réécrire, de mettre en cache et même de synthétiser les réponses. Les images ne sont pas différentes. Lorsque les indices client sont activés, le ServiceWorker actif peut identifier les requêtes d'image, inspecter les indices client fournis et définir sa propre logique de traitement.

self.onfetch = function(event) {
    var req = event.request.clone();
    console.log("SW received request for: " + req.url)
    for (var entry of req.headers.entries()) {
    console.log("\t" + entry[0] +": " + entry[1])
    }
    ...
}
Indices client serviceWorker.

ServiceWorker vous offre un contrôle complet côté client sur la sélection des ressources. Cette étape est cruciale. Réfléchissez-y, car les possibilités sont presque infinies:

  • Vous pouvez réécrire les valeurs d'en-tête d'indicateurs client définies par l'user-agent.
  • Vous pouvez ajouter des valeurs d'en-têtes d'indications client à la requête.
  • Vous pouvez réécrire l'URL et rediriger la requête d'image vers un autre serveur (par exemple, un CDN).
    • Vous pouvez même déplacer les valeurs d'indice des en-têtes vers l'URL elle-même si cela facilite le déploiement dans votre infrastructure.
  • Vous pouvez mettre en cache les réponses et définir votre propre logique pour les ressources diffusées.
  • Vous pouvez adapter votre réponse en fonction de la connectivité des utilisateurs.
  • Vous pouvez tenir compte des forçages d'application et de préférences utilisateur.
  • Vous pouvez vraiment faire tout ce que vous voulez.

L'élément picture fournit le contrôle d'orientation artistique nécessaire dans la balise HTML. Les indices client fournissent des annotations sur les requêtes d'images générées qui permettent d'automatiser la sélection des ressources. ServiceWorker fournit des fonctionnalités de gestion des requêtes et des réponses sur le client. C'est le Web extensible en action.

Questions fréquentes sur les indices client

  1. Où les indices client sont-ils disponibles ? Disponible dans Chrome 46. En cours d'examen dans Firefox et Edge.

  2. Pourquoi les indices client sont-ils activés ? Nous souhaitons réduire les coûts pour les sites qui n'utilisent pas d'indices client. Pour activer les indices client, le site doit fournir l'en-tête Accept-CH ou une directive <meta http-equiv> équivalente dans le balisage de la page. Si l'un de ces éléments est présent, l'user-agent ajoute les indices appropriés à toutes les requêtes de sous-ressources. À l'avenir, nous pourrons fournir un mécanisme supplémentaire pour conserver cette préférence pour une origine particulière, ce qui permettra de transmettre les mêmes indices aux requêtes de navigation.

  3. Pourquoi avons-nous besoin d'indices client si nous disposons d'un ServiceWorker ? ServiceWorker n'a pas accès aux informations de mise en page, de ressources et de largeur de la fenêtre d'affichage. Du moins, sans entraîner des aller-retours coûteux et retarder considérablement la requête d'image (par exemple, lorsqu'une requête d'image est lancée par l'analyseur de préchargement). Les indices client s'intègrent au navigateur pour mettre ces données à disposition dans la requête.

  4. Les indices client ne sont-ils réservés qu'aux ressources image ? Le cas d'utilisation principal derrière les indices DPR, Viewport-Width et Width est d'activer la sélection de ressources pour les composants Image. Toutefois, les mêmes indices sont envoyés pour toutes les sous-ressources, quel que soit leur type. Par exemple, les requêtes CSS et JavaScript reçoivent également les mêmes informations et peuvent également être utilisées pour optimiser ces ressources.

  5. Pourquoi certaines requêtes d'images ne signalent-elles pas la largeur ? Le navigateur peut ne pas connaître la largeur d'affichage prévue, car le site s'appuie sur la taille intrinsèque de l'image. Par conséquent, l'indice de largeur est omis pour ces requêtes et pour celles qui ne comportent pas de "largeur d'affichage" (par exemple, une ressource JavaScript). Pour recevoir des indications de largeur, veillez à spécifier une valeur de taille sur vos images.

  6. Et <insert my favorite hint> ? ServiceWorker permet aux développeurs d'intercepter et de modifier (par exemple, d'ajouter de nouveaux en-têtes) toutes les requêtes sortantes. Par exemple, il est facile d'ajouter des informations basées sur NetInfo pour indiquer le type de connexion actuel. Consultez la section Rapports sur les fonctionnalités avec ServiceWorker. Les indices "natifs" fournis dans Chrome (DPR, largeur, largeur de la ressource) sont implémentés dans le navigateur, car une implémentation purement basée sur le logiciel retarderait toutes les requêtes d'image.

  7. Où puis-je en savoir plus, voir d'autres démonstrations et découvrir d'autres fonctionnalités ? Consultez le document explicatif et n'hésitez pas à signaler un problème sur GitHub si vous avez des commentaires ou d'autres questions.