L'API Intersection Observer en JavaScript
Apprenez l'API Intersection Observer en JavaScript pour détecter efficacement quand un élément entre ou quitte le viewport — chargement différé, défilement infini et animations au défilement.
L'API Intersection Observer vous permet de demander au navigateur de vous avertir quand un élément entre ou quitte la partie visible de la page. Elle le fait de manière efficace et asynchrone, sans le coût de performance lié à l'écoute manuelle des événements de défilement. C'est l'outil approprié pour le chargement différé des images, la construction d'un défilement infini, le déclenchement d'animations lors de l'apparition du contenu, et pour mesurer si une publicité ou une bannière a réellement été vue.
Le problème qu'elle résout
Avant l'existence de cette API, répondre à la simple question « cet élément est-il à l'écran en ce moment ? » était étonnamment pénible. Il fallait attacher un écouteur à l'événement scroll (et souvent resize), puis appeler getBoundingClientRect() sur chaque élément suivi pour comparer sa position par rapport au viewport.
// The old, expensive way — runs on every scroll tick.
window.addEventListener('scroll', () => {
const rect = element.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
if (inView) {
// do something
}
});Les événements de défilement se déclenchent des dizaines de fois par seconde, et getBoundingClientRect() force le navigateur à recalculer la mise en page (un « reflow »). Effectuer ce travail de manière synchrone sur le fil principal pendant un défilement est une source classique de saccades. (Voir Gestion des événements dans le DOM et Défilement JavaScript pour comprendre le comportement de ces événements.)
IntersectionObserver inverse le modèle. Au lieu que vous interrogiez les positions, le navigateur surveille les éléments pour vous et rappelle seulement quand la visibilité change réellement. Le travail se fait hors du fil principal, sans bloquer le défilement. Pour en savoir plus sur l'importance de cela, lisez Optimisation des performances du DOM. Il est un proche cousin de l'API MutationObserver, qui surveille les changements de structure du DOM plutôt que de visibilité.
Utilisation de base
Vous créez un observateur avec un callback, puis vous lui indiquez quels éléments surveiller avec observe().
// 1. Create an observer with a callback and (optional) options.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('Element is now visible:', entry.target);
} else {
console.log('Element left the viewport:', entry.target);
}
});
});
// 2. Start watching a target element.
const target = document.querySelector('#box');
observer.observe(target);Le callback reçoit un array d'entrées, une par élément observé dont la visibilité a changé. Un seul observateur peut surveiller de nombreux éléments, et c'est le pattern recommandé — créez un seul observateur et appelez observe() pour chaque cible plutôt que de créer un observateur par élément.
Le callback s'exécute de manière asynchrone et les changements sont regroupés — le navigateur peut signaler plusieurs entrées en un seul appel. Il se déclenche également une fois juste après le début de l'observation, ce qui vous donne l'état de visibilité initial de l'élément sans attendre un défilement. La compatibilité navigateur est excellente sur tous les navigateurs modernes.
Configuration de l'observateur
Le second argument du constructeur est un object d'options comportant trois propriétés.
root
L'élément utilisé comme viewport pour la vérification de la visibilité. La cible doit être un descendant du root. Quand root est null (par défaut), le viewport propre du navigateur est utilisé.
const observer = new IntersectionObserver(callback, {
root: document.querySelector('#scroll-container'),
});rootMargin
Une marge autour du root, écrite comme une valeur CSS margin. Elle agrandit ou réduit la boîte utilisée pour les vérifications d'intersection. Une astuce courante est d'utiliser une marge inférieure positive pour que les éléments soient signalés comme « visibles » avant d'entrer réellement dans le viewport — utile pour charger le contenu en avance.
const observer = new IntersectionObserver(callback, {
// Trigger 200px before the element reaches the bottom edge.
rootMargin: '0px 0px 200px 0px',
});threshold
Un nombre de 0 à 1, ou un array de nombres, indiquant à l'observateur à quels ratios de visibilité se déclencher. 0 signifie « se déclencher dès qu'un seul pixel est visible », 1 signifie « se déclencher uniquement quand l'élément est entièrement visible ». Un array se déclenche à chaque ratio listé.
const observer = new IntersectionObserver(callback, {
// Fire at 0%, 50%, and 100% visibility.
threshold: [0, 0.5, 1],
});Ce que contient une entrée
Chaque object dans l'array entries décrit la visibilité d'un élément au moment où le callback s'est exécuté. Les propriétés les plus utiles sont :
isIntersecting— un boolean :truesi l'élément est actuellement visible dans le root.intersectionRatio— quelle proportion de l'élément est visible, de0à1.target— l'élément observé.boundingClientRect— la taille et la position de la cible.intersectionRect— la partie visible de la cible.rootBounds— le rectangle du root (ajusté parrootMargin).time— un horodatage du moment où le changement a été enregistré.
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
console.log(entry.target.id, 'visible:', entry.isIntersecting);
console.log('ratio:', entry.intersectionRatio.toFixed(2));
}
});Méthodes : observe, unobserve, disconnect
Une instance d'observateur vous offre trois méthodes :
observe(element)— commencer à surveiller un élément.unobserve(element)— arrêter de surveiller un élément.disconnect()— arrêter de surveiller tous les éléments à la fois.
Une bonne pratique essentielle : une fois qu'un élément a accompli sa tâche ponctuelle — par exemple, une image dont le chargement différé est terminé — appelez unobserve() sur lui pour que le navigateur cesse de suivre quelque chose qui ne changera plus jamais.
Cas d'usage 1 — Chargement différé des images
Le chargement différé reporte le téléchargement des images jusqu'à ce qu'elles soient sur le point d'être vues. Placez l'URL réelle dans un attribut data-src, observez chaque image, et échangez-la dans src quand elle devient visible — puis arrêtez de l'observer.
<img data-src="photo-1.jpg" alt="First photo" width="600" height="400" />
<img data-src="photo-2.jpg" alt="Second photo" width="600" height="400" />
<img data-src="photo-3.jpg" alt="Third photo" width="600" height="400" />const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src; // load the real image
img.removeAttribute('data-src');
observer.unobserve(img); // job done — stop watching it
});
}, { rootMargin: '0px 0px 200px 0px' }); // start loading a little early
images.forEach((img) => imageObserver.observe(img));Les navigateurs modernes prennent également en charge l'attribut natif loading="lazy" sur <img> et <iframe>, qui ne nécessite aucun JavaScript. Utilisez IntersectionObserver quand vous avez besoin d'un comportement personnalisé — un remplacement de placeholder, un fondu, ou le chargement de contenu non-image.
Cas d'usage 2 — Défilement infini
Pour le défilement infini, placez un élément « sentinel » vide au bas de la liste. Quand ce sentinel défile dans le viewport, chargez la page de données suivante et ajoutez-la. Comme le sentinel reste en bas, le même observateur continue de se déclencher au fur et à mesure que l'utilisateur fait défiler.
<ul id="list"></ul>
<div id="sentinel"></div>const list = document.querySelector('#list');
const sentinel = document.querySelector('#sentinel');
let page = 1;
let loading = false;
async function loadMore() {
if (loading) return; // guard against overlapping loads
loading = true;
const res = await fetch('/api/items?page=' + page);
const items = await res.json();
items.forEach((item) => {
const li = document.createElement('li');
li.textContent = item.title;
list.appendChild(li);
});
page += 1;
loading = false;
}
const scrollObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
});
scrollObserver.observe(sentinel);Cas d'usage 3 — Animations au défilement
Un effet populaire consiste à faire apparaître des éléments en fondu ou en glissement à mesure qu'ils entrent dans le viewport. Gardez l'animation en CSS et laissez JavaScript ajouter une classe au bon moment.
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}const revealItems = document.querySelectorAll('.reveal');
const revealObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target); // animate only once
}
});
}, { threshold: 0.15 }); // fire when ~15% is showing
revealItems.forEach((el) => revealObserver.observe(el));Cas d'usage 4 — Suivi des impressions et de la visibilité
L'analyse nécessite souvent de savoir si le contenu a réellement été vu, pas seulement présent dans le DOM. Un threshold plus élevé vous permet d'enregistrer une impression uniquement quand une portion significative d'un élément est visible.
const adObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio >= 0.5) {
sendImpression(entry.target.dataset.adId);
adObserver.unobserve(entry.target); // count each ad once
}
});
}, { threshold: 0.5 }); // at least 50% visible
document.querySelectorAll('.ad').forEach((ad) => adObserver.observe(ad));Vous pourriez enrichir ceci avec un minuteur pour exiger, par exemple, une seconde complète à 50 % de visibilité avant de comptabiliser une impression — un standard courant pour les publicités « viewable ».
Résumé
L'API Intersection Observer remplace les écouteurs de défilement fragiles et gourmands en performances par une manière propre et asynchrone de réagir à la visibilité des éléments. Créez un observateur, pointez-le sur vos cibles avec observe(), lisez isIntersecting et intersectionRatio dans le callback, et appelez unobserve() une fois qu'un élément a terminé son rôle. Avec root, rootMargin et threshold, vous pouvez régler précisément le moment où il se déclenche — rendant le chargement différé, le défilement infini, les animations au défilement et le suivi des impressions à la fois simples et fluides.