24 mai 2024
Le composant Next.js <Image>
9 minutes de lecture
Les images sont devenues des éléments indispensables d'un site web, améliorant grandement l'expérience utilisateur. Mais, implémentées de la mauvaise manière, ces mêmes images peuvent aussi ralentir le temps de chargement des pages, impacter négativement votre SEO et dégrader l'expérience utilisateur.
C'est pourquoi le framework Next.js propose son composant <Image>
, une extension de l'élément HTML <img>
qui contraint à implémenter nos images de la meilleure façon possible.
Le but de cet article est d'expliquer de façon clair le rôle et le fonctionnement parfois mal compris de ce composant.
Il sera divisé en trois parties :
- Les avantages apportés par
<Image>
. - Comment fonctionne
<Image>
? - Puis enfin, des exemples d'utilisation courante.
Depuis la version 13 des changements majeurs ont été introduit sur ce composant. Cet article traitera d'<Image>
uniquement pour les versions supérieures ou égales à next@13.0.0
.
Quels bénéfices apportent <Image>
?
Avant de rentrer en détail dans le fonctionnement de ce composant il est important de comprendre ses avantages.
Une grande partie des optimisations proposée par le composant <Image>
de Next.js ne sont qu'une simple abstraction de l'API de l'élément HTML <img>
. Mais certaines fonctionnalités tirent parti des possibilités du meta-framework Next.js.
En somme, <Image>
agit sous 3 aspects pour améliorer les images de votre site web.
1. Implémentation SEO friendly
Que ça soit par l'accessibilité (avec l'obligation d'utiliser la propriété alt
) ou d'autres aspects tels que l'optimisation du Cumulative Layout Shift (CLS), le composant <Image>
force une implémentation stricte et rigoureuse des images.
2. Images optimisées et responsives
Grâce à son infrastructure client/serveur, Next.js génère divers tailles et formats afin de servir l'image la plus adaptée à l'utilisateur.
Si le navigateur le supporte, un format moderne sera envoyé (comme le WebP). Différentes dimensions d'images seront mises à disposition de <img>
via la propriété srcset.
3. Loading intelligent
Par défaut, toutes vos images seront chargées uniquement si elles sont visibles dans le viewport du navigateur, afin de réduire le poids des médias téléchargés lors du chargement initial de la page (via l'attribut loading
natif).
De belles promesses, mais comment utiliser <Image>
pour en tirer le meilleur parti ?
II. Comment fonctionne <Image>
?
Pour expliquer l'API d' <Image>
et son fonctionnement, je vais devoir m'appuyer sur les propriétés les plus couramment utilisées. C'est une liste non exhaustive afin que vous compreniez mieux son utilisation mais vous pouvez retrouver la liste complète des propriétés ici.
src
Propriété représentant soit un chemin vers une image (interne ou externe) soit une image statique importée.
import Image from 'next/image'
import someStaticImage from './image.png'
function StaticImage() {
return <Image src={profilePic} />
}
function ExternalImage() {
return <Image src="https://example.com/image.png" />
}
function InternalImage() {
return <Image src="/image.png" />
}
Dans le cas d'images externes il sera nécessaire d'autoriser le domaine dans le fichier .nextconfig.js
via la propriété remotePattern
. Étant donné que Next.js expose un endpoint d'API publique pour optimiser les tailles d'images (ex: .../_next/image?url=/logo.png&w=100&h=100&q=70
) des personnes malveillantes pourraient utiliser votre serveur pour optimiser leurs images. Il est donc nécessaire de restreindre l'accès à des domaines de confiance.
alt
Utilisée pour décrire l'image aux lecteurs d'écran et aux moteurs de recherche afin d'améliorer l'accessibilité, cette propriété est obligatoire avec <Image>
. Plus aucune excuse pour oublier le texte alternatif !
width / height
Définit la largeur et hauteur de l'image en pixels rendue sur la page.
Si il n'est pas possible de connaître ces valeurs ou quelles sont dynamiques vous pouvez utiliser la propriété fill
(voir plus bas).
Il est obligatoire de définir ces propriétés sauf si vous utilisez des images statiques (elles seront automatiquement remplies) ou si vous utilisez la propriété fill
.
Une des raisons principales de l'existence de ces propriétés est l'optimisation du Cumulative Layout Shift (CLS).
Si vous ne connaissez pas encore les dimensions de votre image rendue sur la page, appliquez votre style sans vous soucier des ces propriétés. Une fois que vous avez défini votre style, utilisez l'inspecteur de votre navigateur pour obtenir les dimensions de l'image rendu dans l'onglet "Computed" de la devtool.
fill
Booléen indiquant si l'image doit remplir entièrement le conteneur parent. Utile lorsque la largeur ou la hauteur de l'image est inconnue.
Si vous optez pour ce mode votre élément <img>
passe en position: absolute;
et du style lui sera appliqué afin qu'il puisse couvrir entièrement l'élément parent. Il sera nécessaire d'ajouter position: "relative"
, position: "fixed"
, ou position: "absolute"
sur le parent. Et dans certains cas il sera aussi préférable d'ajouter object-fit: "contain"
ou object-fit: "cover”
sur l'élément image, selon le rendu souhaité.
sizes
Chaîne de caractères, similaire à une media query qui donne une information sur la largeur de l'image à différents breakpoints.
Peut-être une des propriétés les plus méconnues du composant <Image>
, pourtant celle-ci affecte grandement les performances (elle n'est pas spécifique à Next.js).
Pour faire court, couplé avec la propriété srcset cette valeur va définir quelle taille d'image doit être téléchargée par le navigateur. Heureusement pour nous la propriété srcset
est gérée automatiquement par <Image>
.
Comment ça fonctionne ?
Quand le navigateur télécharge puis lit le HTML qui contient votre balise <img>
il n'a pas encore chargé le style et ne sait donc pas la taille que va prendre votre image dans le viewport.
Pour connaître cette information il se réfère à la propriété sizes
qui spécifie quelle taille fera notre image en fonction de la largeur du viewport.
C'est en se basant sur celui-ci qu'il sait quelle taille d'image télécharger.
Si vous ne définissez pas cette propriété et que vous utilisez fill
, Next.js va automatiquement définir une valeur par défaut de 100vw
et une large liste de srcset
sera générée (640w/750w/etc..).
Si vous ne définissez pas cette propriété sans utiliser fill
alors une très petite liste de srcset
sera générée (x1/x2/etc).
Il est recommandé de définir cette propriété pour améliorer les performances de votre site surtout
pour les images utilisant fill
ou les images qui sont "responsives".
Pour mieux comprendre, voici un exemple de code Next.js avec une image de 7360 x 4912 pixels.
import Image from 'next/image'
import src from '../../public/desert.png' // 7360 x 4912 pixels
export default function Home() {
return (
<main className="flex justify-center items-center">
<Image
alt="desert"
className="md:w-40 w-screen"
src={src}
placeholder="blur"
sizes="(max-width: 768px) 100vw, 160px"
/>
</main>
)
}
J'ai défini la propriété sizes
à (max-width: 768px) 100vw, 160px
. Donc Next.js va générer un srcset
avec différentes tailles d'images.
Du côté du navigateur, cela se traduira par ce code HTML.
<main className="flex justify-center items-center">
<img
alt="desert"
width="7360"
height="4912"
class="md:w-40 w-screen"
sizes="(max-width: 768px) 100vw, 160px"
srcset="
/_next/image?[...].jpeg&w=640&q=75 640w,
/_next/image?[...].jpeg&w=750&q=75 750w,
/_next/image?[...].jpeg&w=828&q=75 828w,
/_next/image?[...].jpeg&w=1080&q=75 1080w,
/_next/image?[...].jpeg&w=1200&q=75 1200w,
/_next/image?[...].jpeg&w=1920&q=75 1920w,
/_next/image?[...].jpeg&w=2048&q=75 2048w,
/_next/image?[...].jpeg&w=3840&q=75 3840w
"
src="/_next/image?[...].jpeg&w=3840&q=75"
/>
</main>
- Si le viewport est supérieur à 768px de largeur: c'est la taille 640w du
srcset
(la plus proche de 160px) qui sera téléchargé. - Sinon: le browser téléchargera la taille la plus proche du viewport (100vw).
Sans cette optimisation c'est l'image originale (7360 x 4912) qui aurait été chargée sans distinction (ou presque ⬇️)
Les plus attentifs d'entre vous auront remarqué que la taille de l'image dans src
(w=3840
)
n'est pas la taille originale (7360px). C'est une optimisation de Next.js qui considère que la
taille maximum d'un viewport utilisateur est de 3840px. Cette option est configurable dans votre
fichier de
configuration.
placeholder
Chaine de caractères qui sera utilisée pendant que l'image charge. Les valeurs possibles sont
empty
: Valeur par défaut, pas de placeholder.blur
: Si l'image est une image statique le blur fonctionne "out-of-the-box" et une version basse qualité et floutée de l'image sera utilisée pendant le chargement. Sinon il faudra spécifier, en plus, la propriété blurData pour définir l'image basse qualité (base64).data:image/…
: Pour utiliser une URL de données pendant que l'image charge.
III. Exemples d'utilisations courant
J'aimerais vous montrer deux cas pratiques pour vous aider à mieux appréhender ce composant. Pour ces exemples j'utiliserais TailwindCSS pour simplifier la lisibilité du code.
- Image d'arrière-plan statique
- Gallerie d'images dynamique
1. Image d'arrière-plan statique
Voici les liens vers la démo et le code source.
Dans cet exemple, nous allons créer une image d'arrière-plan (avec un léger filtre) qui couvrira entièrement l'écran.
J'utilise la propriété fill
pour que l'image couvre entièrement l'écran.
Comme l'image est en position: absolute;
je n'aurai pas d'effet néfaste sur le Cumulative Layout Shift (CLS), mais il sera nécessaire d'ajouter un élément parent avec une position relative.
import Image from 'next/image'
import src from '../../../public/arrakis.jpeg'
export default function Home() {
return (
<section className="relative w-screen h-screen flex justify-center items-center overflow-hidden">
<h1 className="relative z-30 text-4xl sm:text-8xl text-center">Welcome to Arrakis</h1>
<div className="absolute w-full h-full z-20 top-0 left-0 bottom-0 right-0 bg-black/10" />
<div className="absolute w-full h-full z-10 top-0 left-0 bottom-0 right-0 overflow-hidden">
<Image src={src} alt="desert" placeholder="blur" fill className="object-cover" />
</div>
</section>
)
}
J'utilise une image statique donc le blur
est implémenté par Next.js. Je n'ai donc pas besoin
d'ajouter la propriété blurData
pour définir l'image basse qualité.
2. Galerie d'images dynamiques
Vous trouverez ici la démo de cet exemple et ici le code source.
Dans cet exemple, nous allons créer une galerie d'images responsives dans une grille.
Voici notre composant <Grid>
qui va afficher les images.
function Grid({ children }) {
return (
<section className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 px-4 py-8 md:px-6 md:py-12">
{children}
</section>
)
}
Pour ceux qui ne seraient pas familiers avec TailwindCSS voici un petit résumé du style appliqué:
- Pour les écrans de taille
sm
(< 640px) la grille aura une seule colonne. - Pour les écrans de taille
md
(< 768px) la grille aura deux colonnes. - Pour les écrans de taille
lg
(< 1024px) la grille aura trois colonnes. - Pour les écrans de taille
xl
(>= 1024px) la grille aura quatre colonnes.
Ensuite nous allons créer un composant <Card>
qui va afficher une image (ainsi qu'un titre et une description).
function Card({ src, alt, title, description }) {
return (
<div className="bg-white rounded-lg overflow-hidden shadow-md dark:bg-gray-950">
<div className="w-full h-48 xl:h-72 relative">
<Image
alt={alt}
className="object-cover"
fill
src={src}
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
/>
</div>
<div className="p-4">
<h3 className="text-lg font-semibold mb-2">{title}</h3>
<p className="text-gray-500 dark:text-gray-400">{description}</p>
</div>
</div>
)
}
Je souhaite utiliser la propriété fill
car mes images sont dynamiques et je ne connais pas leurs tailles.
Pour les raisons de performances que nous avons plus haut j'ai défini la propriété sizes
pour que le navigateur sache quelle taille d'image télécharger en fonction de la largeur du viewport.
Je me réfère à mon composant container <Grid>
pour définir la taille de mes images en fonction de la largeur du viewport:
- Pour les écrans de taille
sm
(< 640px) la taille de l'image sera de 100% de la largeur du viewport. - Pour les écrans de taille
md
(< 768px) la taille de l'image sera de 50% de la largeur du viewport. - Pour les écrans de taille
lg
(< 1024px) la taille de l'image sera de 33% de la largeur du viewport. - Pour les écrans de taille
xl
(>= 1024px) la taille de l'image sera de 25% de la largeur du viewport.
Conclusion
Le composant <Image>
nous contraint à une implémentation rigoureuse mais optimale de nos images. Il simplifie la gestion des images en déployant une variété de technique pour améliorer les performances et l'accessibilité de votre site web.
Dans la grande majorité des cas, <Image>
est le choix le plus judicieux pour implémenter vos images dans un projet Next.js. Mais si vous avez des besoins spécifiques ou des contraintes particulières, il est toujours possible d'utiliser l'élément HTML <img>
classique.
J'ajouterai, de façon générale, si vous rencontrez des problèmes de performances avec vos images et que vous utilisez déjà <Image>
, la propriété sizes
est souvent une bonne première piste à explorer.