Comment charger les images React à la demande sans nuire au LCP ?
Pour charger une image à la demande dans React, affichez un élément img
normal avec loading="lazy". Conservez l’URL réelle dans
src, ajoutez srcSet et
sizes si des variantes adaptatives sont disponibles, et fournissez
width et height pour que le navigateur puisse
réserver de l’espace.
L’exception importante est l’image susceptible de devenir l’élément Largest Contentful Paint (LCP) de la page. Gardez son chargement immédiat et rendez-la détectable dans le HTML initial. La charger à la demande retarde la requête dont la page a le plus besoin pour se charger rapidement.
| Rôle de l’image | Mode de chargement | Autres éléments importants du balisage |
|---|---|---|
| Image principale ou LCP probable | Immédiat | fetchPriority="high" après vérification |
| Autre image visible dans la zone initiale | Immédiat | Dimensions et sources adaptatives |
| Image de contenu ordinaire hors écran | À la demande | src, srcSet, sizes et dimensions |
| Arrière-plan CSS décoratif | Mécanisme distinct | Ne pas remplacer un contenu img porteur de sens |
« Au-dessus de la ligne de flottaison » ne correspond pas à un nombre fixe d’images. La hauteur de la zone d’affichage, les mises en page adaptatives, les bannières et les contenus localisés peuvent faire entrer une même image dans la zone d’affichage initiale ou l’en faire sortir. Classez les images selon la page rendue, puis vérifiez votre choix sur des tailles d’écran représentatives.
Utiliser la prop native de chargement de React
React transmet la prop loading à l’élément image natif du navigateur. Pour
une image ordinaire hors écran, vous n’avez besoin ni d’un effet, ni d’un gestionnaire de
défilement, ni d’une bibliothèque Intersection Observer.
L’aperçu local suivant effectue le rendu de React côté serveur et envoie au navigateur le balisage complet des images. Vous pouvez réutiliser les composants dans une application React existante avec rendu côté serveur. Cet aperçu nécessite Node.js et Corepack installé séparément. Utilisez une version maintenue de Node.js ; cet aperçu a été testé avec Node.js 24.15.0 et 26.8.1, Yarn 4.12.0, React 19.2.6 et Chrome 151. Il s’agit des versions testées, pas d’un minimum intrinsèque pour le chargement natif des images. Les commandes utilisent un shell POSIX.
Créez un nouveau répertoire react-image-preview vide et ouvrez-y un terminal.
Enregistrez package.json :
{
"name": "react-image-preview",
"private": true,
"type": "module",
"packageManager": "yarn@4.12.0",
"dependencies": {
"react": "19.2.6",
"react-dom": "19.2.6"
},
"devDependencies": {
"esbuild": "0.27.0"
}
}
Enregistrez .yarnrc.yml pour sélectionner les dépendances locales et ignorer
l’exécutable Yarn d’un projet parent :
nodeLinker: node-modules
enableGlobalCache: false
ignorePath: true
Créez le fichier de verrouillage avant l’installation pour délimiter le projet, afin que Yarn traite ce répertoire comme un projet distinct :
touch yarn.lock && corepack yarn install
Si l’installation échoue, corrigez le problème de réseau ou de gestionnaire de paquets signalé, puis relancez cette commande dans le même répertoire. Conservez vos fichiers sources.
Créez un répertoire images contenant ces quatre fichiers WebP issus de vos
propres photos. Chaque paire doit présenter le même cadrage ; les largeurs indiquées dans
srcSet doivent correspondre aux largeurs réelles des fichiers en pixels.
Modifiez le texte alternatif pour décrire vos photos.
| Fichier | Dimensions en pixels | Rôle |
|---|---|---|
images/harbor-640.webp | 640×360 | Petite variante principale |
images/harbor-1280.webp | 1280×720 | Grande variante principale et repli |
images/workshop-480.webp | 480×320 | Petite variante de contenu |
images/workshop-960.webp | 960×640 | Grande variante de contenu et repli |
Enregistrez ce composant sous le nom ResponsiveImage.tsx :
import type { ReactNode } from 'react'
interface ResponsiveImageProps {
alt: string
height: number
sizes: string
src: string
srcSet: string
width: number
}
export function ResponsiveImage({
alt,
height,
sizes,
src,
srcSet,
width,
}: ResponsiveImageProps): ReactNode {
return (
<img
className="responsive-image"
src={src}
srcSet={srcSet}
sizes={sizes}
width={width}
height={height}
loading="lazy"
decoding="async"
alt={alt}
/>
)
}
Conservez une véritable URL d’image dans src. Déplacer la seule URL
dans un attribut personnalisé data-src rend la requête dépendante de votre
JavaScript et empêche le scanner de préchargement du navigateur de la détecter dans le balisage
normal des images. L’image reste également non chargée si le script échoue.
La prop decoding="async" est une indication distincte concernant le décodage. Elle
ne décide pas du moment où la requête réseau démarre et ne remplace pas
loading="lazy".
Enregistrez ImagePage.tsx. Le grand espace vide place la photo d’atelier bien
au-delà de la zone d’affichage initiale pour cette expérience de chargement ; cette mise en page
n’est pas à reproduire dans votre application.
import type { ReactNode } from 'react'
import { HeroImage } from './HeroImage.tsx'
import { ResponsiveImage } from './ResponsiveImage.tsx'
export function ImagePage(): ReactNode {
return (
<main>
<h1>React image preview</h1>
<section className="hero-column">
<HeroImage />
<p>The harbor photo loads with the page.</p>
</section>
<section className="demo-spacer">
<p>Scroll to the workshop photo.</p>
</section>
<section className="image-column">
<ResponsiveImage
src="/images/workshop-960.webp"
srcSet="/images/workshop-480.webp 480w, /images/workshop-960.webp 960w"
sizes="(max-width: 40rem) 100vw, 40rem"
width={960}
height={640}
alt="A technician calibrating a camera rig"
/>
<p>The workshop photo keeps this caption in place while it loads.</p>
</section>
</main>
)
}
Le chargement à la demande change uniquement le moment où la requête démarre. Il ne redimensionne pas l’image, ne la compresse pas, ne la convertit pas, ne la met pas en cache et ne la décrit pas.
Garder le chargement de l’image LCP immédiat
N’appliquez pas loading="lazy" à une image susceptible d’être l’élément LCP.
Incluez son URL dans le HTML initial lorsque votre framework React prend en charge le rendu côté
serveur, afin que le navigateur puisse la détecter sans attendre le rendu côté client ou une autre
requête de données.
Enregistrez HeroImage.tsx :
import type { ReactNode } from 'react'
export function HeroImage(): ReactNode {
return (
<img
className="responsive-image"
src="/images/harbor-1280.webp"
srcSet="/images/harbor-640.webp 640w, /images/harbor-1280.webp 1280w"
sizes="(max-width: 80rem) 100vw, 80rem"
width={1280}
height={720}
loading="eager"
fetchPriority="high"
alt="Fishing boats returning to the harbor at sunrise"
/>
)
}
fetchPriority="high" indique une priorité relative, sans garantir l’ordre des requêtes.
Réservez-le à l’image identifiée comme LCP par les mesures. Si plusieurs images ont une priorité
élevée, le navigateur dispose d’informations moins utiles pour décider laquelle compte le plus.
React 19 peut aussi générer automatiquement un préchargement d’image lors du rendu côté serveur. Inspectez le HTML obtenu avant d’ajouter un préchargement manuel : la documentation de React sur les images décrit les conditions. Un préchargement doit utiliser les mêmes variantes adaptatives et la même largeur d’emplacement que l’image.
Une image peut être l’élément LCP sur mobile mais pas sur ordinateur, ou l’inverse. Utilisez des données de performance réelles pour confirmer quel élément devient le LCP pour les visiteurs, plutôt que de supposer que toute grande image ou diapositive de carrousel est critique.
Combiner le chargement à la demande avec des images adaptatives
Une requête différée peut tout de même télécharger un fichier inutilement volumineux. Avec des
descripteurs de largeur comme 480w, srcSet
fournit les fichiers candidats et sizes décrit la largeur de
l’emplacement affiché. Le navigateur combine ces informations avec la densité de pixels de
l’appareil pour choisir une variante.
Définissez sizes en fonction de la mise en page, pas du fichier source.
Dans cet aperçu, la photo d’atelier remplit la zone d’affichage jusqu’à une colonne de 40rem, et
l’image principale la remplit jusqu’à 80rem. Le CSS ci-dessous applique ces limites. Avec la taille
de police racine de 16 pixels de l’aperçu, elles correspondent à 640 et 1 280 pixels CSS. Des
marges intérieures ou un conteneur plus étroit dans votre application nécessitent une valeur
sizes réduite en conséquence.
Le navigateur effectue la même sélection de variantes adaptatives pour les images à chargement
immédiat et celles chargées à la demande. Le mode de chargement ne dispense pas de fournir des
variantes correctes et une valeur sizes précise. Le navigateur peut
réutiliser une variante plus grande déjà en cache, et les écrans à haute densité peuvent nécessiter
plus de pixels que ces deux variantes n’en fournissent. Vérifiez currentSrc
avec un cache vide plutôt que de considérer un nom de fichier comme un choix universel. Consultez
les images adaptatives pour le calcul de la largeur d’emplacement.
Réserver de l’espace pour éviter les décalages de mise en page
Fournissez width et height pour chaque image de
contenu. Les navigateurs utilisent ces attributs pour calculer un rapport largeur/hauteur avant le
téléchargement de l’image, afin de lui réserver la bonne quantité d’espace dans la mise en page.
Votre CSS peut toujours rendre l’image fluide. Enregistrez preview.css :
html {
font-size: 16px;
color-scheme: light dark;
}
body {
margin: 0;
}
.responsive-image {
display: block;
width: 100%;
height: auto;
}
.hero-column {
max-width: 80rem;
margin-inline: auto;
}
.image-column {
max-width: 40rem;
margin-inline: auto;
}
.demo-spacer {
min-height: 6000px;
}
Les attributs doivent décrire le rapport largeur/hauteur intrinsèque de l’image. Par exemple, un
img peut utiliser width={960} et
height={640} lorsque chaque variante a un rapport de 3:2, même si le navigateur
sélectionne un fichier de 480×320. Si des sources choisies selon la direction artistique utilisent
des cadrages ou des rapports largeur/hauteur différents, leurs dimensions doivent décrire la
source sélectionnée plutôt qu’une image de repli sans rapport avec elle.
Réserver de l’espace pour les images élimine une source de Cumulative Layout Shift (CLS), mais pas tous les décalages possibles. Les légendes, les publicités, les contrôles de consentement, les polices et les messages d’erreur peuvent encore déplacer le contenu de la page.
Exécuter l’aperçu avec rendu côté serveur
Enregistrez preview.tsx. Ce serveur ne sert que cette page et les quatre
fichiers connus sur votre interface de boucle locale. Il lit les fichiers au démarrage ; une photo
ou une feuille de style manquante arrête donc l’aperçu avant qu’il n’annonce une URL prête à être
ouverte. Cet aperçu statique ne comporte ni hydratation ni configuration de déploiement en
production.
import { readFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { renderToStaticMarkup } from 'react-dom/server'
import { ImagePage } from './ImagePage.tsx'
async function main(): Promise<void> {
const images = new Map<string, Buffer>()
for (const name of [
'harbor-640.webp',
'harbor-1280.webp',
'workshop-480.webp',
'workshop-960.webp',
]) {
images.set(`/images/${name}`, await readFile(new URL(`./images/${name}`, import.meta.url)))
}
const css = await readFile(new URL('./preview.css', import.meta.url), 'utf8')
const html = '<!doctype html>' + renderToStaticMarkup(
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React image preview</title>
<style>{css}</style>
</head>
<body><ImagePage /></body>
</html>,
)
const port = Number(process.env.PORT ?? 4173)
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error('PORT must be an integer between 0 and 65535')
}
const server = createServer((request, response) => {
response.setHeader('Cache-Control', 'no-store')
const image = images.get(request.url ?? '')
if (image) {
response.writeHead(200, { 'Content-Type': 'image/webp' })
response.end(image)
} else if (request.url === '/') {
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
response.end(html)
} else {
response.writeHead(404)
response.end('Not found')
}
})
server.on('error', (error) => {
console.error(`Could not start image preview: ${error.message}`)
process.exitCode = 1
})
process.once('SIGINT', () => { server.close() })
server.listen(port, '127.0.0.1', () => {
const address = server.address()
if (address && typeof address !== 'string') {
console.log(`Image preview: http://127.0.0.1:${address.port}/`)
}
})
}
main().catch((error: unknown) => {
console.error(error)
process.exitCode = 1
})
Compilez et lancez l’aperçu depuis son répertoire. Le paramètre --tsconfig-raw vide
empêche la configuration TypeScript d’un projet parent de modifier l’environnement d’exécution JSX
de cet aperçu. Cette opération remplace le fichier preview.mjs généré ; elle
ne modifie pas vos photos. Le && empêche une compilation échouée de
lancer un ancien fichier généré.
corepack yarn exec esbuild preview.tsx --bundle --platform=node --format=esm --packages=external --jsx=automatic --tsconfig-raw='{}' --outfile=preview.mjs && node preview.mjs
Ouvrez l’URL affichée et faites défiler la page jusqu’à la photo d’atelier. Appuyez sur Ctrl+C dans le même terminal pour arrêter le serveur. Si le démarrage signale un fichier manquant, corrigez son nom ou son emplacement, puis relancez la commande de compilation et de lancement. Après une erreur signalant un port occupé, choisissez un port disponible dans un shell POSIX :
PORT=0 node preview.mjs
Le serveur affichera la nouvelle URL.
Quand Intersection Observer est-il adapté ?
Utilisez le comportement de chargement natif du navigateur pour les images ordinaires. Ne recourez
à Intersection Observer que lorsque le composant nécessite un comportement que
loading="lazy" ne peut pas exprimer, comme démarrer une animation, enregistrer un
événement de visibilité ou appliquer un arrière-plan décoratif coûteux peu avant son arrivée dans
la zone d’affichage.
Ne montez pas conditionnellement une image de contenu ordinaire uniquement après le déclenchement
d’un observateur, sauf si ce comportement est réellement nécessaire. Le montage conditionnel
retarde la détection, ajoute un risque d’échec lié au JavaScript et vous oblige à concevoir une
solution de repli stable. Les images porteuses de sens doivent rester des éléments
img ou picture dotés d’un texte alternatif utile.
React.lazy() résout un autre problème : il diffère le chargement d’un module de
composant JavaScript. Il ne diffère pas automatiquement les requêtes d’images rendues par ce
composant.
Générer des variantes adaptatives avec Transloadit
Le chargement à la demande ne peut pas corriger une image source trop volumineuse. Un pipeline de diffusion pratique génère un petit ensemble limité de largeurs, optimise chaque résultat, les exporte vers un stockage durable et enregistre leurs URL et leurs dimensions pour la vue React.
Le Robot /image/resize de Transloadit peut créer des variantes de largeur, et le Robot /image/optimize peut optimiser les formats pris en charge. Générez les variantes avant le rendu de la page, exportez-les vers votre stockage et enregistrez leurs dimensions réelles avec leurs URL durables. Une source plus petite que la largeur cible peut produire un résultat plus petit ; n’associez pas à cette URL un descripteur de largeur auquel elle ne correspond pas. Consultez le service d’exportation de fichiers pour les intégrations de stockage, ou Smart CDN pour les transformations à la demande.
L’application React reste responsable des choix de présentation : quelle image est porteuse de sens, son texte alternatif, la largeur de son emplacement affiché et son caractère critique au chargement initial. Transloadit prend en charge l’étape de traitement des médias plutôt que de décider quelle image devient le LCP dans une mise en page donnée.
Mesurer le résultat
Vérifiez la page en production plutôt que de vous fier uniquement au code source du composant :
- Inspectez la réponse HTML initiale. Les attributs
srcetsrcsetde l’image principale devraient déjà être présents. - Enregistrez des chargements avec des zones d’affichage étroites et larges et un cache vide dans les outils de développement du navigateur. Confirmez que la requête de l’image principale démarre à partir de ce HTML, sans attendre le JavaScript de l’application.
- En haut de l’aperçu, vérifiez que la photo d’atelier située plus loin n’a pas fait l’objet d’une requête. Faites défiler la page vers elle et observez le démarrage de sa requête. Le chargement natif à la demande peut récupérer l’image avant qu’elle ne soit visible.
- Inspectez le
currentSrcde chaque image et ouvrez cette URL pour vérifier les dimensions du fichier en pixels. La valeurnaturalWidthde l’élément peut être corrigée en fonction de la densité. Comparez le fichier sélectionné avec l’emplacement affiché et le rapport de pixels de l’appareil. Par exemple, dans cet aperçu, Chrome avec un cache vide et un rapport de pixels de 1 a sélectionné le fichier de la photo d’atelier de 480 pixels dans une zone d’affichage de 390 pixels, et le fichier de 960 pixels pour la colonne de 640 pixels sur ordinateur. - Retardez les réponses des images et comparez le cadre de l’image et la position de la légende avant et après le chargement. Bloquez également les requêtes pour vérifier le texte alternatif et l’espace réservé en cas d’échec.
Ces vérifications établissent le comportement de détection, de sélection et de mise en page sur une page locale. Elles ne prouvent pas une amélioration du LCP pour les visiteurs. Dans votre application, identifiez l’élément LCP réel et mesurez le LCP et le CLS avec des données réelles, regroupées par modèle de page et zone d’affichage pertinente. Les recommandations de web.dev sur le LCP expliquent comment la détection des ressources contribue au délai de chargement.
Les distances et la planification du chargement à la demande dans les navigateurs dépendent de l’implémentation. Évitez une règle comme « charger à la demande toutes les images après la troisième » ou l’hypothèse d’un seuil fixe en pixels. Mesurez les pages et les appareils réellement utilisés par vos utilisateurs.
Erreurs courantes de chargement à la demande dans React
- Charger à la demande l’image principale ou l’image identifiée comme LCP par les mesures.
- Déplacer la seule URL de l’image de
srcversdata-src. - N’afficher les images critiques qu’après l’exécution du code côté client ou la fin d’une requête de données.
- Fournir
srcSetsans valeursizesprécise. - Omettre
widthetheightparce que le CSS finit par contrôler la taille de l’image. - Marquer chaque image avec
fetchPriority="high". - Utiliser une bibliothèque d’observateurs uniquement pour reproduire le comportement natif du navigateur.
- Supposer que
React.lazy()contrôle les requêtes réseau des images. - Différer le chargement d’une source de 3 000 pixels au lieu de générer une variante adaptée.
Pour les règles sous-jacentes du navigateur, consultez la norme HTML sur le chargement à la demande. Lorsque les scripts sont désactivés, les navigateurs chargent immédiatement ces images prévues pour le chargement natif à la demande ; conserver les URL réelles dans le balisage permet tout de même au contenu de se charger.
