Un composant Image encapsule les bonnes pratiques en matière de performances et fournit une solution prête à l'emploi pour optimiser les images.
Les images sont une source courante de problèmes de performances pour les applications Web et un domaine d'optimisation clé. Les images non optimisées contribuent à l'augmentation de la taille des pages et représentent plus de 70% du poids total des pages en octets au 90th centile. Il existe plusieurs façons d'optimiser les images. Il est donc nécessaire d'avoir un "composant d'image" intelligent avec des solutions de performances intégrées par défaut.
L'équipe Aurora a collaboré 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 constitue un bon modèle et définit 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. Cet article explique comment nous avons conçu le composant Image de Next.js et les enseignements que nous en avons tirés.

Problèmes et opportunités 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 indicateur de conversions des utilisateurs visitant des sites Web. Les sessions au cours desquelles les utilisateurs ont effectué une conversion comportaient 38% moins d'images que celles où ils n'en ont pas effectué. Lighthouse liste plusieurs opportunités pour optimiser les images et améliorer les signaux Web essentiels dans le cadre de son audit des bonnes pratiques. Voici quelques-uns des domaines courants dans lesquels les images peuvent avoir un impact sur 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 soit spécifiée peuvent entraîner une instabilité de la mise en page et contribuer à un Cumulative Layout Shift (CLS) élevé. 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 significative dans le format peut entraîner une déformation de l'image. Une propriété relativement nouvelle qui vous permet de spécifier le format dans le code CSS peut vous aider à redimensionner les images de manière responsive tout en évitant le CLS.
Les grandes images peuvent nuire au LCP
Plus la taille du fichier image est importante, plus le téléchargement prendra du temps. Une grande image peut être l'image "hero" 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 essentiel et dont le téléchargement prend beaucoup de temps retardera le LCP.
Dans de nombreux cas, les développeurs peuvent réduire la taille des images en améliorant la compression et en utilisant des 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 alors choisir la bonne image 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 comme AVIF ou WebP peuvent offrir une meilleure compression que les formats JPEG et PNG couramment utilisés. Une meilleure compression réduit la taille du fichier de 25% à 50% dans certains cas, pour une qualité d'image identique. Cette réduction permet d'accélérer les téléchargements et de consommer moins de données. L'application doit diffuser des formats d'image modernes aux navigateurs compatibles.
Le chargement d'images inutiles nuit au LCP
Les images situées sous la ligne de flottaison ou hors du champ d'affichage ne sont pas présentées à l'utilisateur lorsque la page est chargée. Ils peuvent être différés afin de ne pas contribuer au LCP et de le retarder. Le chargement différé peut être utilisé pour charger ces images ultérieurement, lorsque l'utilisateur fait défiler la page vers le bas.
Difficultés liées à l'optimisation
Les équipes peuvent évaluer le coût des performances en raison des problèmes énumérés précédemment et mettre en œuvre des solutions de bonnes pratiques pour les surmonter. Cependant, cela ne se produit souvent pas 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 l'optimisation du code, de JavaScript et des données. Il est donc possible qu'ils ne soient pas au courant des problèmes liés aux images ni de la façon de les optimiser. Les images créées par des designers ou importées par des utilisateurs peuvent ne pas figurer en haut de la liste des priorités.
- Solution prête à l'emploi : même si les développeurs connaissent les 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 un frein.
- Images dynamiques : en plus des images statiques qui font partie de l'application, les images dynamiques sont importées par les utilisateurs ou proviennent de bases de données externes ou de CMS. Il peut être difficile de définir la taille de telles images lorsque leur source est dynamique.
- Surcharge de balisage : les solutions permettant d'inclure la taille d'image ou
srcsetpour différentes tailles nécessitent un balisage supplémentaire pour chaque image, ce qui peut être fastidieux. L'attributsrcseta été introduit en 2014, mais il n'est utilisé que par 26,5%des sites Web aujourd'hui. Lorsque vous utilisezsrcset, les développeurs doivent créer des images de différentes tailles. Des outils tels que just-gimme-an-img peuvent vous aider, mais vous devez les utiliser manuellement pour chaque image. - Compatibilité avec les navigateurs : les formats d'image modernes comme AVIF et WebP créent des fichiers image plus petits, mais nécessitent une gestion spéciale sur 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 sur tous les navigateurs. - Complications liées au chargement différé : il existe plusieurs techniques et bibliothèques permettant d'implémenter le chargement différé pour les images situées sous la ligne de flottaison. Choisir la meilleure peut être difficile. Les développeurs peuvent également ne pas connaître la meilleure distance par rapport à la ligne de flottaison pour charger les images différées. Les différentes tailles de fenêtre d'affichage sur les appareils peuvent compliquer davantage la situation.
- Évolution du paysage : à mesure que les navigateurs prennent en charge de nouvelles fonctionnalités HTML ou CSS pour améliorer les performances, il peut être difficile pour les développeurs de les évaluer toutes. Par exemple, Chrome lance la fonctionnalité Fetch Priority en tant qu'Origin Trial. Il peut être utilisé pour augmenter la priorité de certaines images sur la page. Globalement, les développeurs trouveraient plus facile d'évaluer et d'implémenter ces améliorations au niveau des composants.
Composant Image comme solution
Les opportunités disponibles pour optimiser les images et les difficultés liées à leur implémentation individuelle pour chaque application nous ont conduit à l'idée d'un composant d'image. Un composant Image peut encapsuler et appliquer les bonnes pratiques. En remplaçant l'élément <img> par un composant d'image, les développeurs peuvent mieux résoudre leurs problèmes de performances d'image.
Au cours de l'année écoulée, nous avons collaboré avec le framework Next.js pour concevoir et implémenter leur 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 complet de fonctionnalités et de principes. Il inclut également des options permettant aux développeurs de le personnaliser pour répondre à diverses exigences concernant les images.
Protection contre les modifications de mise en page
Comme nous l'avons vu précédemment, les images sans dimensions entraînent des décalages de mise en page et contribuent au CLS. Lorsqu'ils utilisent le composant Image de 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 sans taille 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 d'éviter les décalages de mise en page.
Faciliter la réactivité
Pour que les images soient responsives sur tous les appareils, les développeurs doivent définir les attributs srcset et sizes dans l'élément <img>. Nous avons voulu réduire cet effort avec le composant Image. Nous avons conçu le composant Image Next.js pour définir les valeurs des attributs une seule fois par application. Nous les appliquons à toutes les instances du composant Image en fonction du mode de mise en page. Nous avons trouvé une solution en trois parties :
- Propriété
deviceSizes: cette propriété peut être utilisée pour configurer les points d'arrêt une seule fois en fonction des appareils courants de la base d'utilisateurs de l'application. Les valeurs par défaut des points d'arrêt sont incluses dans le fichier de configuration. - 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 la taille de l'appareil. - Attribut
layoutdans chaque image : il permet d'indiquer comment utiliser les propriétésdeviceSizesetimageSizespour chaque image. Les valeurs acceptées pour le mode de mise en page sontfixed,fill,intrinsicetresponsive.
Lorsqu'une image est demandée avec les modes de mise en page responsive ou fill, Next.js identifie l'image à diffuser en fonction de la taille de l'appareil qui demande la page et définit les attributs srcset et sizes de 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 de 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 sur une fenêtre d'affichage plus grande. La largeur du conteneur est de 100% | |
|
|
| Mise en page = Fixe : l'image n'est pas responsive. La largeur et la hauteur sont fixes, comme pour l'élément ` |
|
|
|
| Mise en page = Responsive : l'annonce est réduite ou agrandie en fonction de la largeur du conteneur sur différentes fenêtres d'affichage, tout en conservant les proportions. | |
|
|
Mise en page = Remplir : la largeur et la hauteur sont étirées pour remplir le conteneur parent. (Dans cet exemple, la largeur du parent <div> est définie sur 300*500.)
|
|
|
|
Fournir un chargement différé intégré
Le composant Image fournit une solution de chargement différé performante et intégrée par défaut. 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: il est compatible avec tous les navigateurs récents. - Utilisez l'API Intersection Observer : la création d'une solution de chargement différé 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 charger les images de manière différée : il peut être nécessaire de déployer des efforts supplémentaires pour évaluer et intégrer une bibliothèque tierce appropriée pour le chargement différé.
Dans le composant Image 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 récents. Les développeurs n'ont aucune action supplémentaire à effectuer pour l'activer, mais ils peuvent la désactiver si nécessaire.
Précharger les images importantes
Les éléments LCP sont souvent des images, et les images volumineuses peuvent retarder le LCP. Il est conseillé de précharger les images critiques pour que le navigateur puisse les découvrir plus rapidement. Lorsque vous utilisez un élément <img>, un indice de préchargement peut être inclus dans l'en-tête HTML comme suit.
<link rel="preload" as="image" href="important.png">
Un composant d'image bien conçu doit permettre de modifier la séquence de chargement des images, quel que soit le framework utilisé. Dans le cas du composant Image de 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 Image.
<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 des options pour appliquer des 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 comme WebP et AVIF. Le composant Image Next.js utilise un CDN d'images par défaut à 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 de l'image. Le framework concatène l'URL relative avec le chemin 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.
Proposer des images auto-hébergées
Il peut arriver que des sites Web ne puissent pas utiliser de CDN d'images. Dans ce cas, un composant d'image doit prendre en charge les images auto-hébergées. Le composant Image Next.js utilise un optimiseur d'images en tant que serveur d'images intégré qui fournit une API de type 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 d'images.
Prise en charge du chargement progressif
Le chargement progressif est une technique utilisée pour maintenir l'intérêt des utilisateurs en affichant un espace réservé pour une image (généralement de qualité nettement inférieure) pendant que l'image réelle se charge. Cela améliore les performances perçues et l'expérience utilisateur. Il peut être utilisé en combinaison avec le chargement différé pour les images au-dessous de la ligne de flottaison ou au-dessus.
Le composant Image Next.js prend en charge le chargement progressif de l'image via la propriété placeholder. Il peut être utilisé comme LQIP (espace réservé pour une image de basse qualité) pour afficher une image de basse qualité ou floue pendant le chargement de l'image réelle.
Impact
Grâce à toutes ces optimisations, nous avons réussi à utiliser le composant Image de Next.js en production. Nous travaillons également avec d'autres piles technologiques sur des composants d'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 Next.js. Sur une page migrée de <img> vers next/image, le temps 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 à chargement différé).
Enseignements à tirer
Toute personne qui crée une application Next.js peut bénéficier de l'utilisation du 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 leçons que nous avons apprises en cours de route et qui pourraient vous être utiles.
Les soupapes de sécurité peuvent faire plus de mal que de bien
Dans une version préliminaire du composant Image de 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 rendu l'attribut unsized obsolète et l'avons supprimé.
Distinguer les frictions utiles des désagréments inutiles
L'obligation de redimensionner une image est un exemple de "friction utile". Il limite l'utilisation du composant, mais offre en contrepartie des avantages considérables en termes de performances. 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 sacrifier les performances. Par exemple, lors du développement du composant Image Next.js, nous avons reçu des plaintes concernant la difficulté à trouver 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.
Trouver un équilibre entre les fonctionnalités pratiques et les optimisations des performances
Si votre composant d'image n'impose qu'une "friction utile" à ses utilisateurs, les développeurs auront tendance à ne pas vouloir l'utiliser. Nous avons constaté que les fonctionnalités liées aux performances, comme le redimensionnement des images et la génération automatique des valeurs srcset, étaient les plus importantes. Les fonctionnalités pratiques destinées aux développeurs, comme le chargement différé 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.
Définir une feuille de route pour les fonctionnalités afin de favoriser 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 ces cas, ce composant n'est pas pour vous".
En pratique, cette stratégie s'avère incompatible avec vos objectifs en tant que concepteur de composants. Vous souhaitez que les développeurs adoptent votre composant pour bénéficier de ses avantages en termes de performances. Il est difficile de le faire si un groupe d'utilisateurs ne peut pas migrer et se sent exclu de la conversation. Ils risquent d'exprimer leur déception, ce qui peut entraîner une perception négative et affecter l'adoption.
Il est conseillé d'établir une feuille de route pour votre composant, qui couvre tous les cas d'utilisation raisonnables à long terme. Il est également utile d'indiquer clairement dans la documentation 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 bon é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 tâche coûteuse, mais à fort impact.
Au lieu de laisser chaque application réinventer la roue à chaque fois, nous avons créé un modèle de bonnes pratiques que les développeurs, les frameworks et d'autres piles technologiques pourraient utiliser comme référence pour leurs propres implémentations. Cette expérience s'avérera précieuse lorsque nous prendrons en charge d'autres frameworks sur leurs composants d'image.
Le composant Image de Next.js a permis d'améliorer les performances des applications Next.js, et donc l'expérience utilisateur. Nous pensons qu'il s'agit d'un excellent modèle qui fonctionnerait bien dans l'écosystème au sens large. Nous aimerions entendre les développeurs qui souhaitent l'adopter dans leurs projets.