Créer un composant d'image efficace

Un composant Image encapsule les bonnes pratiques en matière de performances et fournit une solution prête à l'emploi pour optimiser les images.

Leena Sohoni
Leena Sohoni
Kara Erickson
Kara Erickson
Alex Castle
Alex Castle

Les images sont souvent à l'origine de goulots d'étranglement qui affectent les performances des applications Web, et constituent un domaine clé de l'optimisation. Les images non optimisées contribuent à l'encombrement des pages et représentent plus de 70% du poids total de la page en octets au 90th percentile. Les différentes façons d'optimiser les images nécessitent un "composant d'image" intelligent avec des solutions de performances intégrées par défaut.

L'équipe Aurora a travaillé avec Next.js pour créer un tel composant. L'objectif était de créer un modèle d'image optimisé que les développeurs Web pourraient personnaliser davantage. Ce composant sert de bon modèle et établit une norme pour la création de composants d'image dans d'autres frameworks, systèmes de gestion de contenu (CMS) et piles technologiques. Nous avons collaboré sur un composant similaire pour Nuxt.js et nous travaillons avec Angular sur l'optimisation des images dans les futures versions. Ce post explique comment nous avons conçu le composant Next.js Image et présente les enseignements que nous en avons tirés.

Composant Image en tant qu'extension des images

Opportunités et problèmes d'optimisation des images

Les images ont un impact non seulement sur les performances, mais aussi sur l'activité. Le nombre d'images sur une page était le deuxième meilleur prédicteur de conversions pour les utilisateurs qui visitent des sites Web. Les sessions au cours desquelles les utilisateurs ont réalisé une conversion comportaient 38% d'images en moins que les sessions au cours desquelles ils n'ont pas réalisé de conversion. Lighthouse liste plusieurs opportunités d'optimiser les images et d'améliorer les métriques Web essentielles dans le cadre de son audit des bonnes pratiques. Voici quelques-uns des cas courants où les images peuvent affecter les Core Web Vitals et l'expérience utilisateur.

Les images non redimensionnées nuisent au CLS

Les images diffusées sans que leur taille ne soit spécifiée peuvent entraîner une instabilité de la mise en page et contribuer à une valeur élevée de Cumulative Layout Shift (CLS). Définir les attributs width et height sur les éléments img peut aider à éviter les décalages de mise en page. Exemple :

<img src="flower.jpg" width="360" height="240">

La largeur et la hauteur doivent être définies de sorte que le format de l'image affichée soit proche de son format naturel. Une différence importante de format peut entraîner une distorsion de l'image. Une propriété relativement nouvelle qui vous permet de spécifier des formats dans CSS peut vous aider à dimensionner les images de manière réactive tout en évitant le CLS.

Les images volumineuses peuvent nuire au LCP

Plus la taille d'un fichier image est importante, plus le téléchargement est long. Une image de grande taille peut être l'image "héros" de la page ou l'élément le plus important de la fenêtre d'affichage responsable du déclenchement du Largest Contentful Paint (LCP). Une image qui fait partie du contenu critique et dont le téléchargement prend beaucoup de temps retarde le LCP.

Dans de nombreux cas, les développeurs peuvent réduire la taille des images grâce à une meilleure compression et à l'utilisation d'images responsives. Les attributs srcset et sizes de l'élément <img> permettent de fournir des fichiers image de différentes tailles. Le navigateur peut ensuite choisir la bonne en fonction de la taille et de la résolution de l'écran.

Une mauvaise compression des images peut nuire au LCP

Les formats d'image modernes tels que AVIF ou WebP peuvent offrir une meilleure compression que les formats JPEG et PNG couramment utilisés. Une meilleure compression réduit la taille des fichiers de 25 à 50 % dans certains cas pour la même qualité d'image. Cette réduction permet d'accélérer les téléchargements et de consommer moins de données. L'application doit diffuser des images modernes dans les navigateurs compatibles.

Le chargement d'images inutiles nuit au LCP

Les images situées en dessous de la ligne de flottaison ou qui ne figurent pas dans la fenêtre d'affichage ne sont pas visibles par l'utilisateur lorsque la page est chargée. Ils peuvent être différés afin qu'ils ne contribuent pas à la LCP et ne la retardent pas. Le chargement différé peut être utilisé pour charger ces images ultérieurement lorsque l'utilisateur les atteint.

Défis d'optimisation

Les équipes peuvent évaluer le coût des performances en raison des problèmes listés précédemment et mettre en œuvre des solutions conformes aux bonnes pratiques pour les résoudre. Toutefois, ce n'est souvent pas le cas dans la pratique, et les images inefficaces continuent de ralentir le Web. Les raisons suivantes peuvent expliquer ces différences :

  • Priorités: les développeurs Web ont généralement tendance à se concentrer sur le code, JavaScript et l'optimisation des données. Par conséquent, ils ne sont peut-être pas au courant des problèmes liés aux images ni de la façon de les optimiser. Les images créées par des concepteurs ou importées par des utilisateurs ne sont pas forcément prioritaires.
  • Solution prête à l'emploi: même si les développeurs sont conscients des nuances de l'optimisation des images, l'absence d'une solution tout-en-un prête à l'emploi pour leur framework ou leur pile technologique peut être dissuasive.
  • Images dynamiques: outre les images statiques intégrées à l'application, les images dynamiques sont importées par les utilisateurs, ou issues de bases de données externes ou de CMS. Il peut être difficile de définir la taille de ces images dont la source est dynamique.
  • Surcharge de balisage: les solutions permettant d'inclure la taille de l'image ou srcset pour différentes tailles nécessitent un balisage supplémentaire pour chaque image, ce qui peut être fastidieux. L'attribut srcset a été introduit en 2014, mais il n'est utilisé que par 26,5% des sites Web à l'heure actuelle. Lorsque les développeurs utilisent srcset, ils doivent créer des images de différentes tailles. Des outils tels que just-gimme-an-img peuvent vous aider, mais ils doivent être utilisés manuellement pour chaque image.
  • Compatibilité avec les navigateurs: les formats d'image modernes tels que AVIF et WebP créent des fichiers d'image plus petits, mais nécessitent une gestion spéciale dans les navigateurs qui ne les prennent pas en charge. Les développeurs doivent utiliser des stratégies telles que la négociation de contenu ou l'élément <picture> pour que les images soient diffusées dans tous les navigateurs.
  • Complications liées au chargement différé: plusieurs techniques et bibliothèques sont disponibles pour implémenter le chargement différé des images en dessous de la ligne de flottaison. Choisir la meilleure peut s'avérer difficile. Les développeurs ne connaissent peut-être pas non plus la meilleure distance à partir du "pli" pour charger les images différées. Les différentes tailles de fenêtre d'affichage sur les appareils peuvent encore compliquer la tâche.
  • Évolution du paysage: étant donné que les navigateurs commencent à accepter de nouvelles fonctionnalités HTML ou CSS pour améliorer les performances, il peut être difficile pour les développeurs d'évaluer chacune d'elles. Par exemple, Chrome lance la fonctionnalité Priorité de récupération en tant que phase d'évaluation d'origine. Il permet d'augmenter la priorité d'images spécifiques sur la page. Globalement, les développeurs trouveraient plus simple que ces améliorations soient évaluées et implémentées au niveau des composants.

Composant Image en tant que solution

Les possibilités d'optimisation des images et les difficultés d'implémentation individuelle pour chaque application nous ont conduits à imaginer un composant Image. Un composant d'image peut encapsuler et appliquer les bonnes pratiques. En remplaçant l'élément <img> par un composant Image, les développeurs peuvent mieux résoudre les problèmes de performances des images.

Au cours de l'année écoulée, nous avons travaillé avec le framework Next.js pour concevoir et implémenter son composant Image. Il peut être utilisé comme remplacement direct des éléments <img> existants dans les applications Next.js, comme suit.

// Before with <img> element:
function Logo() {
  return <img src="/logo.jpg" alt="logo" height="200" width="100" />
}

// After with image component:
import Image from 'next/image'

function Logo() {
  return <Image src="/logo.jpg" alt="logo" height="200" width="100" />
}

Le composant tente de résoudre les problèmes liés aux images de manière générique grâce à un ensemble riche de fonctionnalités et de principes. Elle inclut également des options permettant aux développeurs de la personnaliser en fonction de diverses exigences d'image.

Protection contre les changements de mise en page

Comme indiqué précédemment, les images non dimensionnées entraînent des décalages de mise en page et contribuent au CLS. Lorsque vous utilisez le composant Image Next.js, les développeurs doivent fournir une taille d'image à l'aide des attributs width et height pour éviter tout décalage de mise en page. Si la taille est inconnue, les développeurs doivent spécifier layout=fill pour diffuser une image non dimensionnée qui se trouve dans un conteneur dimensionné. Vous pouvez également utiliser des importations d'images statiques pour récupérer la taille de l'image réelle sur le disque dur au moment de la compilation et l'inclure dans l'image.

// Image component with width and height specified
<Image src="/logo.jpg" alt="logo" height="200" width="100" />

// Image component with layout specified
<Image src="/hero.jpg" layout="fill" objectFit="cover" alt="hero" />

// Image component with image import
import Image from 'next/image'
import logo from './logo.png'

function Logo() {
  return <Image src={logo} alt="logo" />
}

Étant donné que les développeurs ne peuvent pas utiliser le composant Image sans taille, la conception garantit qu'ils prendront le temps de réfléchir à la taille des images et à éviter les décalages de mise en page.

Faciliter la réactivité

Pour rendre les images responsives sur tous les appareils, les développeurs doivent définir les attributs srcset et sizes dans l'élément <img>. Nous voulions réduire cet effort avec le composant Image. Nous avons conçu le composant Image Next.js pour définir les valeurs d'attribut une seule fois par application. Nous les appliquons à toutes les instances du composant Image en fonction du mode de mise en page. Nous avons élaboré une solution en trois parties:

  1. Propriété deviceSizes: cette propriété permet de configurer des points d'arrêt ponctuellement en fonction des appareils communs à la base d'utilisateurs de l'application. Les valeurs par défaut des points d'arrêt sont incluses dans le fichier de configuration.
  2. Propriété imageSizes: il s'agit également d'une propriété configurable utilisée pour obtenir les tailles d'image correspondant aux points d'arrêt de taille de l'appareil.
  3. Attribut layout dans chaque image: permet d'indiquer comment utiliser les propriétés deviceSizes et imageSizes pour chaque image. Les valeurs acceptées pour le mode de mise en page sont fixed, fill, intrinsic et responsive.

Lorsqu'une image est demandée avec les modes de mise en page responsive (adaptatif) ou fill (remplissage), Next.js identifie l'image à diffuser en fonction de la taille de l'appareil qui demande la page et définit srcset et sizes dans l'image de manière appropriée.

La comparaison suivante montre comment le mode de mise en page peut être utilisé pour contrôler la taille de l'image sur différents écrans. Nous avons utilisé une image de démonstration partagée dans la documentation Next.js, affichée sur un téléphone et un ordinateur portable standard.

Écran d'ordinateur portable Écran du téléphone
Mise en page = intrinsèque: la mise en page est réduite pour s'adapter à la largeur du conteneur sur les fenêtres d'affichage plus petites. Ne s'agrandit pas au-delà de la taille intrinsèque de l'image dans une fenêtre d'affichage plus grande. La largeur du conteneur est de 100%
Image des montagnes telle quelle Image des montagnes réduite
Mise en page = Fixe: l'image n'est pas responsive. La largeur et la hauteur sont fixes de la même manière que l'élément , quel que soit l'appareil sur lequel elle est affichée.
Image des montagnes telle quelle L&#39;image des montagnes affichée telle quelle ne s&#39;adapte pas à l&#39;écran
Mise en page = Responsive: la mise en page est réduite ou agrandie en fonction de la largeur du conteneur sur différentes fenêtres d'affichage, tout en conservant le format.
Image des montagnes agrandie pour s&#39;adapter à l&#39;écran Image des montagnes réduite pour s&#39;adapter à l&#39;écran
Mise en page = Fill (Remplir) : la largeur et la hauteur sont étirées pour remplir le conteneur parent. (La largeur de l'<div> parente est définie sur 300*500 dans cet exemple)
Image des montagnes affichée pour s&#39;adapter à la taille 300 x 500 Image des montagnes affichée pour s&#39;adapter à la taille 300 x 500
Images affichées pour différentes mises en page

Fournir un chargement différé intégré

Par défaut, le composant Image fournit une solution de chargement différé intégrée et performante. Lorsque vous utilisez l'élément <img>, plusieurs options de chargement différé sont disponibles, mais elles présentent toutes des inconvénients qui les rendent difficiles à utiliser. Un développeur peut adopter l'une des approches de chargement différé suivantes:

  • Spécifiez l'attribut loading, qui est compatible avec tous les navigateurs modernes.
  • Utilisez l'API Intersection Observer: créer une solution de préchargement personnalisée nécessite des efforts, ainsi qu'une conception et une implémentation réfléchies. Les développeurs n'ont pas toujours le temps de le faire.
  • Importer une bibliothèque tierce pour les images à chargement différé: des efforts supplémentaires peuvent s'avérer nécessaires pour évaluer et intégrer une bibliothèque tierce adaptée au chargement différé.

Dans le composant Image de Next.js, le chargement est défini sur "lazy" par défaut. Le chargement différé est implémenté à l'aide d'Intersection Observer, qui est disponible sur la plupart des navigateurs modernes. Les développeurs n'ont rien à faire pour l'activer, mais ils peuvent le désactiver si nécessaire.

Précharger des images importantes

Les éléments LCP sont souvent des images, et les images volumineuses peuvent retarder le LCP. Il est recommandé de précharger les images critiques afin que le navigateur puisse les découvrir plus tôt. Lorsque vous utilisez un élément <img>, une indication de préchargement peut être incluse dans la section "head" du code HTML comme suit.

<link rel="preload" as="image" href="important.png">

Un composant Image bien conçu doit permettre d'ajuster la séquence de chargement des images, quel que soit le framework utilisé. Dans le cas du composant Image Next.js, les développeurs peuvent indiquer une image qui est un bon candidat pour le préchargement à l'aide de l'attribut priority du composant Images.

<Image src="/hero.jpg" alt="hero" height="400" width="200" priority />

L'ajout d'un attribut priority simplifie le balisage et est plus pratique à utiliser. Les développeurs de composants d'image peuvent également explorer les options permettant d'appliquer des méthodes heuristiques afin d'automatiser le préchargement des images au-dessus de la ligne de flottaison sur la page qui répondent à des critères spécifiques.

Encourager l'hébergement d'images hautes performances

Les CDN d'images sont recommandés pour automatiser l'optimisation des images. Ils sont également compatibles avec les formats d'image modernes tels que WebP et AVIF. Le composant Image de Next.js utilise par défaut un CDN d'image à l'aide d'une architecture de chargeur. L'exemple suivant montre que le chargeur permet de configurer le CDN dans le fichier de configuration Next.js.

module.exports = {
  images: {
    loader: 'imgix',
    path: 'https://ImgApp/imgix.net',
  },
}

Avec cette configuration, les développeurs peuvent utiliser des URL relatives dans la source d'image. Le framework concatène l'URL relative avec le chemin du CDN pour générer l'URL absolue. Les CDN d'images populaires tels que Imgix, Cloudinary et Akamai sont compatibles. L'architecture permet d'utiliser un fournisseur de services cloud personnalisé en implémentant une fonction loader personnalisée pour l'application.

Prise en charge des images auto-hébergées

Il peut arriver que les sites Web ne puissent pas utiliser de CDN d'images. Dans ce cas, un composant d'image doit accepter les images auto-hébergées. Le composant Image de Next.js utilise un optimiseur d'images comme serveur d'images intégré qui fournit une API semblable à un CDN. L'optimiseur utilise Sharp pour les transformations d'images de production s'il est installé sur le serveur. Cette bibliothèque est un bon choix pour tous ceux qui souhaitent créer leur propre pipeline d'optimisation des images.

Prendre en charge le chargement progressif

Le chargement progressif est une technique utilisée pour préserver l'intérêt des internautes en affichant une image d'espace réservé, généralement de qualité nettement inférieure, pendant le chargement de l'image. Il améliore les performances perçues et l'expérience utilisateur. Vous pouvez l'utiliser en combinaison avec le chargement paresseux pour les images situées en dessous de la ligne de flottaison ou au-dessus de la ligne de flottaison.

Le composant Image de Next.js est compatible avec le chargement progressif de l'image via la propriété placeholder. Vous pouvez l'utiliser comme espace réservé d'image de faible qualité (LQIP) pour afficher une image de faible qualité ou floue pendant le chargement de l'image réelle.

Impact

Avec toutes ces optimisations intégrées, nous avons constaté le succès du composant Image Next.js en production et nous travaillons également avec d'autres piles technologiques sur des composants Image similaires.

Lorsque Leboncoin a migré son ancien frontend JavaScript vers Next.js, il a également mis à niveau son pipeline d'images pour utiliser le composant Image de Next.js. Sur une page qui a migré de <img> vers next/image, le LCP est passé de 2,4 s à 1,7 s. Le nombre total d'octets d'image téléchargés pour la page est passé de 663 ko à 326 ko (avec environ 100 ko d'octets d'image mis en cache de manière différée).

Enseignements à tirer

Toute personne qui crée une application Next.js peut utiliser le composant Image Next.js pour l'optimisation. Toutefois, si vous souhaitez créer des abstractions de performances similaires pour un autre framework ou CMS, voici quelques enseignements que nous avons tirés en cours de route et qui pourraient vous être utiles.

Les valves de sécurité peuvent faire plus de mal que de bien

Dans une version préliminaire du composant Image Next.js, nous avons fourni un attribut unsized qui permettait aux développeurs de contourner l'exigence de dimensionnement et d'utiliser des images dont les dimensions n'étaient pas spécifiées. Nous avons pensé que cela serait nécessaire dans les cas où il était impossible de connaître à l'avance la hauteur ou la largeur de l'image. Toutefois, nous avons remarqué que les utilisateurs recommandaient l'attribut unsized dans les problèmes GitHub comme solution universelle aux problèmes liés aux exigences de dimensionnement, même dans les cas où ils pouvaient résoudre le problème sans aggraver le CLS. Nous avons ensuite abandonné et supprimé l'attribut unsized.

Distinguer les frictions utiles des ennuis inutiles

L'obligation de redimensionner une image est un exemple de "friction utile". Il limite l'utilisation du composant, mais offre en échange des avantages de performances hors normes. Les utilisateurs accepteront facilement la contrainte s'ils ont une idée claire des avantages potentiels en termes de performances. Il est donc utile d'expliquer ce compromis dans la documentation et les autres documents publiés sur le composant.

Toutefois, vous pouvez trouver des solutions de contournement pour ces frictions sans compromettre les performances. Par exemple, lors du développement du composant Image de Next.js, nous avons reçu des plaintes concernant la difficulté de rechercher les tailles des images stockées localement. Nous avons ajouté des importations d'images statiques, qui simplifient ce processus en récupérant automatiquement les dimensions des images locales au moment de la compilation à l'aide d'un plug-in Babel.

Trouvez le bon équilibre entre fonctionnalités pratiques et optimisation des performances.

Si votre composant Image ne fait que créer des "frictions utiles" pour ses utilisateurs, les développeurs auront tendance à ne pas vouloir l'utiliser. Nous avons constaté que les fonctionnalités de performances telles que le dimensionnement des images et la génération automatique des valeurs srcset étaient les plus importantes. Des fonctionnalités pratiques destinées aux développeurs, comme le chargement paresseux automatique et les espaces réservés flous intégrés, ont également suscité l'intérêt pour le composant Image de Next.js.

Établir une feuille de route pour des fonctionnalités favorisant l'adoption

Il est très difficile de créer une solution qui fonctionne parfaitement dans toutes les situations. Il peut être tentant de concevoir quelque chose qui fonctionne bien pour 75% des utilisateurs, puis de dire aux 25% restants que "dans ce cas, ce composant n'est pas fait pour vous".

En pratique, cette stratégie s'avère être en contradiction avec vos objectifs en tant que concepteur de composants. Vous souhaitez que les développeurs adoptent votre composant afin de profiter de ses avantages en termes de performances. Cela est difficile à faire si un certain nombre d'utilisateurs ne peuvent pas migrer et se sentent exclus de la conversation. Ils sont susceptibles d'exprimer leur déception, ce qui entraîne des perceptions négatives qui affectent l'adoption.

Il est conseillé de disposer d'une feuille de route pour votre composant qui couvre tous les cas d'utilisation raisonnables à long terme. Il est également utile d'être explicite dans la documentation sur ce qui n'est pas pris en charge et pourquoi, afin de définir les attentes concernant les problèmes que le composant est censé résoudre.

Conclusion

L'utilisation et l'optimisation des images sont complexes. Les développeurs doivent trouver le juste équilibre entre les performances et la qualité des images, tout en assurant une expérience utilisateur de qualité. L'optimisation des images est donc une opération coûteuse et à fort impact.

Au lieu de laisser chaque application réinventer la roue à chaque fois, nous avons élaboré un modèle de bonnes pratiques que les développeurs, les frameworks et d'autres piles technologiques peuvent utiliser comme référence pour leurs propres implémentations. Cette expérience sera effectivement précieuse, car nous prenons en charge d'autres frameworks pour leurs composants d'image.

Le composant Image de Next.js a permis d'améliorer les performances des applications Next.js, ce qui a amélioré l'expérience utilisateur. Nous pensons qu'il s'agit d'un excellent modèle qui fonctionnerait bien dans l'écosystème plus large. Nous aimerions connaître l'avis des développeurs qui souhaitent adopter ce modèle dans leurs projets.