Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

L'API Intersection Observer (chargement au défilement)

Apprenez à détecter efficacement quand un élément devient visible avec l'API Intersection Observer, au lieu de l'événement scroll : création d'un observateur, options root, rootMargin et threshold, pr...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Apprenez à détecter efficacement quand un élément devient visible avec l'API Intersection Observer, au lieu de l'événement scroll : création d'un observateur, options root, rootMargin et threshold, propriétés des entrées, et un exemple complet de fil d'actualités avec animations d'apparition, images chargées à la demande et défilement infini. Avec d'autres cas d'usage et des activités corrigées.
  1. Objectifs

    • Comprendre pourquoi l'événement scroll n'est pas adapté pour savoir si un élément est visible.
    • Créer un IntersectionObserver et régler ses options root, rootMargin et threshold.
    • Réaliser trois effets courants : apparition au défilement, images chargées à la demande et défilement infini.
  2. Le problème : savoir si un élément est visible

    • Beaucoup de pages modernes réagissent au défilement : animations qui apparaissent, images chargées au dernier moment, liste qui se complète en bas de page (réseaux sociaux, boutiques en ligne).
    • L'ancienne méthode consistait à écouter l'événement scroll et à calculer la position de chaque élément avec getBoundingClientRect():
    // À éviter : exécuté des dizaines de fois par seconde
    window.addEventListener("scroll", () => {
      for (const element of elements) {
        const rect = element.getBoundingClientRect();
        if (rect.top < window.innerHeight && rect.bottom > 0) {
          element.classList.add("visible");
        }
      }
    });
    • Ce code s'exécute à chaque pixel défilé, oblige le navigateur à recalculer la mise en page et rend la page saccadée, surtout sur téléphone (voir la leçon sur debounce et throttle, chapitre 23).
    • L'API Intersection Observer résout ce problème : on lui confie des éléments, et c'est le navigateur qui nous prévient, de façon optimisée, quand ils entrent dans l'écran ou en sortent.
  3. Créer un observateur

  4. const observateur = new IntersectionObserver(callback, options);
    observateur.observe(element);   // commencer à surveiller un élément
    observateur.unobserve(element); // arrêter de le surveiller
    observateur.disconnect();       // tout arrêter
  • La fonction callback(entrees, observateur) reçoit un tableau d'objets IntersectionObserverEntry, un par élément dont la visibilité a changé. Elle est aussi appelée une première fois juste après observe().
  • Propriétés utiles de chaque entrée :
PropriétéSignification
targetL'élément observé
isIntersectingtrue si l'élément est (au moins en partie) dans la zone
intersectionRatioPart visible de l'élément, de 0 à 1
boundingClientRectPosition et taille de l'élément
  • Les options

  • OptionPar défautRôle
    rootnull (l'écran)Zone de référence : l'écran ou un élément qui défile (une liste avec overflow: auto).
    rootMargin"0px"Agrandit (valeur positive) ou réduit (négative) la zone, comme une marge CSS : "200px 0px" déclenche 200 px avant que l'élément n'arrive à l'écran.
    threshold0Part visible qui déclenche l'appel : 0 (le premier pixel), 0.5 (la moitié), 1 (entièrement visible), ou une liste [0, 0.5, 1].

    Intersection Observer : zone de l'écran, rootMargin, threshold et sentinelle

    1. Exemple complet : un fil d'actualités

    2. La page affiche 30 actualités, chargées par paquets de 5 quand on arrive en bas. Chaque article apparaît en fondu et son image n'est téléchargée qu'au moment où il s'approche de l'écran. Trois observateurs, trois usages.

      index.html:

      <!doctype html>
      <html lang="fr">
        <head>
          <meta charset="utf-8" />
          <title>Actualités – défilement infini</title>
          <link rel="stylesheet" href="style.css" />
          <script src="observer.js" defer></script>
        </head>
        <body>
          <h1>Actualités du centre</h1>
          <div id="liste"></div>
          <p id="sentinelle">Chargement…</p>
        </body>
      </html>

      style.css:

      .article {
        border: 1px solid #ccd;
        border-radius: 8px;
        padding: 16px;
        margin: 12px 0;
        opacity: 0;
        transform: translateY(30px);
        transition: opacity 0.6s, transform 0.6s;
      }
      
      .article.visible {
        opacity: 1;
        transform: none;
      }
      
      .article img {
        width: 100%;
        aspect-ratio: 16 / 9;
        background: #eef2f7; /* réserve la place avant le chargement */
      }

      observer.js:

      const liste = document.getElementById("liste");
      const sentinelle = document.getElementById("sentinelle");
      const PAR_PAGE = 5;
      const TOTAL = 30;
      let page = 0;
      
      // 1. Faire apparaître chaque article quand il entre dans l'écran
      const apparition = new IntersectionObserver(
        (entrees) => {
          for (const entree of entrees) {
            if (entree.isIntersecting) {
              entree.target.classList.add("visible");
              apparition.unobserve(entree.target); // une seule fois
            }
          }
        },
        { threshold: 0.15 }
      );
      
      // 2. Charger l'image seulement quand l'article approche (200 px avant)
      const chargementImages = new IntersectionObserver(
        (entrees, observateur) => {
          for (const entree of entrees) {
            if (entree.isIntersecting) {
              const img = entree.target;
              img.src = img.dataset.src;
              observateur.unobserve(img);
            }
          }
        },
        { rootMargin: "200px 0px" }
      );
      
      function creerArticle(numero) {
        const article = document.createElement("article");
        article.className = "article";
        const titre = document.createElement("h2");
        titre.textContent = `Actualité n° ${numero}`;
        const img = document.createElement("img");
        img.alt = `Illustration de l'actualité ${numero}`;
        img.dataset.src = `https://picsum.photos/seed/${numero}/640/360`;
        article.append(titre, img);
        apparition.observe(article);
        chargementImages.observe(img);
        return article;
      }
      
      function chargerPage() {
        const debut = page * PAR_PAGE + 1;
        for (let numero = debut; numero < debut + PAR_PAGE && numero <= TOTAL; numero++) {
          liste.append(creerArticle(numero));
        }
        page++;
        if (page * PAR_PAGE >= TOTAL) {
          sentinelle.textContent = "Vous avez tout lu 🎉";
          defilementInfini.disconnect();
        }
      }
      
      // 3. Défilement infini : quand la sentinelle devient visible, on charge la suite
      const defilementInfini = new IntersectionObserver((entrees) => {
        if (entrees[0].isIntersecting) {
          chargerPage();
        }
      });
      defilementInfini.observe(sentinelle);
    3. Usage 1 : animation d'apparition

      • Les articles sont invisibles et décalés vers le bas en CSS ; l'observateur leur ajoute la classe visible, et la transition CSS fait le reste.
      • Après l'apparition, unobserve() arrête la surveillance de l'élément : l'animation ne se rejoue pas et le navigateur a moins de travail.
      • Accessibilité : respectez le réglage prefers-reduced-motion des personnes sensibles aux animations (exemple dans le TP 21).
    4. Usage 2 : images chargées à la demande

      • L'adresse de l'image est rangée dans data-src; elle est copiée dans src (ce qui lance le téléchargement) quand l'image arrive à 200 px de l'écran grâce à rootMargin.
      • Bon à savoir : pour une simple image, HTML fait aujourd'hui la même chose sans JavaScript avec l'attribut loading="lazy": <img src="photo.jpg" loading="lazy" alt="…">. Utilisez-le en priorité, et l'observateur pour les cas plus complexes (arrière-plans CSS, vidéos, composants, publicités…).
      • Réservez toujours la place de l'image (width/height ou aspect-ratio) pour éviter que la page ne « saute » au chargement.
    5. Usage 3 : défilement infini

      • Un élément vide placé après la liste, la sentinelle, est observé. Quand il devient visible, on ajoute le paquet suivant : la sentinelle est repoussée plus bas, et ainsi de suite.
      • Quand il n'y a plus rien à charger, disconnect() arrête l'observateur.
      • Dans une vraie application, chargerPage() appelle une API avec fetch et un numéro de page (?page=3). Pensez à empêcher deux chargements simultanés avec une variable enCours.
      • Prévoyez aussi un bouton « Charger plus » : le défilement infini empêche d'atteindre le pied de page et gêne la navigation au clavier.
    6. Autres usages

      • Sommaire actif: mettre en surbrillance, dans un menu, la section en cours de lecture.
      • Vidéos: lancer la lecture quand la vidéo est visible, la mettre en pause sinon.
      • Statistiques: savoir si une publicité ou un bloc a vraiment été vu (threshold: 0.5 pendant 1 seconde).
      • En-tête collant: changer le style du menu quand la bannière du haut sort de l'écran.
    7. Activités

    8. Activité 1 : reproduire et observer

    • Dans le dossier observer de votre dépôt js-api-navigateur, reproduisez l'exemple. Dans l'onglet Réseau des DevTools (filtre Img), vérifiez que les images ne sont téléchargées qu'au défilement.
    • Remplacez rootMargin: "200px 0px" par "0px", puis par "-100px 0px". Que constatez-vous ?
    • Remplacez threshold: 0.15 par 1. Que se passe-t-il pour un article plus haut que l'écran ?

    Activité 2 : sommaire actif

    • Créez une longue page de cours avec 5 sections <section id="…"> et un menu fixe à gauche. Le lien de la section visible doit recevoir la classe active.
    • Astuce : rootMargin: "-40% 0px -55% 0px" ne considère qu'une bande au milieu de l'écran.

    Activité 3 : compteurs animés

    • Affichez trois chiffres clés du centre (stagiaires, formateurs, taux de réussite). Quand le bloc devient visible à 50 %, les nombres montent de 0 à leur valeur en 1,5 seconde (requestAnimationFrame). L'animation ne se joue qu'une fois.

    Activité 4 : questions

    • Pourquoi l'observateur est-il plus performant qu'un écouteur scroll?
    • À quoi sert unobserve() dans l'animation d'apparition ?
    • Quel attribut HTML permet de différer le chargement d'une image sans JavaScript ?
    Corrigés. Activité 1 : avec "0px", les images se chargent au moment où elles entrent dans l'écran (on les voit apparaître) ; avec "-100px 0px", il faut qu'elles soient entrées de 100 px. Avec threshold: 1, un article plus haut que l'écran n'est jamais visible à 100 % et n'apparaît donc jamais. Activité 4 : le navigateur calcule lui-même les intersections de façon optimisée et n'appelle la fonction que lors d'un changement, au lieu de l'appeler à chaque pixel défilé ; unobserve() évite de rejouer l'animation et allège le travail du navigateur ; loading="lazy".
  • À retenir

    • new IntersectionObserver(callback, { root, rootMargin, threshold }) puis observe(element).
    • Dans la fonction, on teste entree.isIntersecting et on agit sur entree.target.
    • Trois usages classiques : apparition au défilement, chargement différé, défilement infini avec une sentinelle.
    • Pour une image simple, préférez loading="lazy".

    Sources : MDN Web Docs, web.dev, W3C, WHATWG, open-meteo.com.