Faire du Lazy Loading avec l’API Intersection Observer (Javascript)

Mathieu Chartier 19 novembre 2018 à 22:44 Tutoriels illustrés 3 commentaires

Mettre en place du Lazy Loading est devenu essentiel en matière d'expérience utilisateur (UX Design) mais aussi pour booster le chargement des pages web. La technique du "chargement paresseux" existe depuis de nombreuses années mais nous allons présenter la méthode la plus en vogue actuellement (et dans un futur proche)...

L'idée du Lazy Loading est de permettre le chargement des images au fur et à mesure du scroll (défilement de la page). Prenons un exemple simple : admettons que votre site contienne 20 images réparties sur l'équivalent de 5 écrans de hauteur. Dans ce cas, le navigateur (ou le robot de moteur de recherche) va charger la totalité des 20 images, alors que la plupart des utilisateurs ne visiteront sûrement pas les 5 écrans de hauteur. C'est donc une charge inutile, à la fois pour la planète (chaque octet économisé préserve la planète, ne l'oublions jamais...), pour le navigateur (ou le robot) et pour les utilisateurs. Avec un chargement paresseux, on s'assure que seules les images dans les écrans visités seront chargées et affichées...

Dans notre cas, nous allons nous intéresser à l'API Intersection Observer, encore en cours de développement mais est supporté sur la plupart des navigateurs actuels (Microsoft Edge 16 et plus, Firefox 55 et plus, Google Chrome 58 et plus, Opera 45 et plus...). Seuls Safari et nos bons vieux Internet Explorer sont exclus de la liste, mais un polyfill fonctionnel permet de contrer facilement le problème.

L'avantage de l'API Intersection Observer est son fonctionnement asynchrone et plus léger que les autres méthodes existantes ou historiques. L'API peut être utilisée pour faire du scroll infini, du Lazy Loading ou d'autres pratiques qui nécessitent d'effectuer des actions en cas d'intersections entre deux éléments (le défilement et les images dans le cas du Lazy Loading...).

Techniquement, plusieurs méthodes en Javascript permettent d'effectuer du Lazy Loading, dont la plupart utilisent le défilement et des calculs mathématiques pour mesurer les intersections entre les images et le niveau de scroll. Cela fonctionne très bien mais reste une forme de "bidouille", souvent lourde en calcul pour le navigateur. Il existe de nombreuses bibliothèques et scripts existants qui usent de cette méthode.

Sans plus attendre, voici comment faire du Lazy Loading avec l'API Intersection Observer en Javascript natif (ou Vanilla JS ^^). Je ne vais pas réinventer la roue, puisque des portions de code proviennent de documentations officielles, je les ai juste réadaptées et complétées, sachant que nous pouvons allons aller encore plus loin (liens des codes à télécharger dans la conclusion du tutoriel).

Étape n°1

Ajoutez les scripts utiles dans l’entête de votre site web, ou avant la fermeture de la balise </body> idéalement.

<!-- Polyfill pour l'API Intersection Observer -->
<script src="CHEMIN/intersection-observer-polyfill.js"></script>

<!-- Script pour le Lazy Loading -->
<!-- (On peut ajouter "async" ou "defer" en attribut) -->
<script src="CHEMIN/intersection-observer-script.js"></script>

 

Étape n°2

Préparez les images côté HTML. En effet, il faut remplacer l’attribut « src » par « data-src » pour que les images ne se chargent pas automatiquement.

<img data-src="CHEMIN/image1.jpg" alt="Image"/>
<img data-src="CHEMIN/image2.jpg" alt="Image"/>
<img data-src="CHEMIN/image3.jpg" alt="Image"/>

 

Étape n°3

Pour respecter le W3C jusqu’au bout, nous devons ajouter un attribut « src » (sinon c’est une « faute »). Soit vous mettez « # » en valeur, soit une image par défaut (légère pour ne pas surcharger le site).

<img src="#" data-src="CHEMIN/image1.jpg" alt="Image"/>
<img src="#" data-src="CHEMIN/image2.jpg" alt="Image"/>
<img src="#" data-src="CHEMIN/image3.jpg" alt="Image"/>


 

Étape n°4

Si vos images possèdent des attributs « srcset », vous pouvez également les renommer en « data-srcset » sur le même principe (sans mettre un « srcset » par défaut).

<img src="#" data-src="CHEMIN/image1.jpg" data-srcset="image1-640.jpg 640w" alt="Image"/>
<img src="#" data-src="CHEMIN/image2.jpg" data-srcset="image2-640.jpg 640w" alt="Image"/>
<img src="#" data-src="CHEMIN/image3.jpg" data-srcset="image3-640.jpg 640w" alt="Image"/>

 

Étape n°5

Si vous avez des images avec un chargement en « background » via CSS, vous pouvez ajouter la classe « bckg-img » aux blocs concernés côté HTML. Cela validera le Lazy Loading pour ces images…

<div id="bloc-perso" class="bckg-img"></div>

<!-- Côté CSS, on aurait ceci par exemple... -->
#bloc-perso {background:url(CHEMIN/image.png) no-repeat;}

 

Étape n°6

Vous avez déjà fini, le Lazy Loading devrait fonctionner. En bonus, je vous mets le code Javascript détaillé ci-dessous ainsi que les fichiers à télécharger… ¨¨^^

Exemple de Lazy Loading

Conclusion

Voici les codes que vous pouvez télécharger (script et polyfill) ci-dessous, avec le détail du script Javascript utilisant l’API Intersection Observer juste en-dessous. Profitez bien… ;-)

Télécharger “lazy-loading.zip”lazy-loading.zip – Téléchargé 926 fois – 7,58 Ko

Mise à jour 1.1 du 13/09/2019

Un problème de compatibilité avec Internet Explorer a été détecté, cela provient de la fonction forEach() en Javascript mal comprise par le navigateur. Un polyfill officiel a été rajouté tout en haut du code pour corriger ce problème, tout rentre donc dans l’ordre. ;-)

P.S. : je vais certainement créer un petit plugin WordPress utilisant cette technique, avec quelques options, etc. C’est déjà en cours de développement pour mon propre usage, je ne sais pas si je le sortirai « officiellement »…

// Polyfill pour forEach pour IE 8 à 11
if (window.NodeList && !NodeList.prototype.forEach) {
    NodeList.prototype.forEach = function (callback, thisArg) {
        thisArg = thisArg || window;
        for (var i = 0; i < this.length; i++) {
            callback.call(thisArg, this[i], i, this);
        }
    };
}

// Fonction de préchargement d'image
function preloadImage(media) {
  var src = media.getAttribute('data-src');
  var srcset = media.getAttribute('data-srcset');
  if(!src) {
    return;
  }
  media.src = src;
  media.removeAttribute("data-src");

  if(!srcset) {
    return;
  }
  media.srcset = srcset;
  media.removeAttribute("data-srcset");
}

// Configuration de l'observer (optionnel)
var config = {
  rootMargin: '0px 0px 100px 0px',
  // threshold: 0  // Incompatible avec Safari, attention !
};

// Instanciation de l'intersectionObserver pour le lazy loading
var observer = new IntersectionObserver(function(entries, self) {
// Pour chaque entrée ciblée (les images ici)
entries.forEach(function(entry) {
    // L'API Javascript vérifie que l'entrée existe...
    if(entry.isIntersecting) {
      if((entry.target.tagName == "VIDEO" || entry.target.tagName == "AUDIO") && entry.target.children.length > 0) {
        for(var source in entry.target.children) {
          var mediaSource = entry.target.children[source];
          if(typeof mediaSource.tagName === "string" && mediaSource.tagName === "SOURCE") {
            // mediaSource.src = mediaSource.dataset.src;
            preloadImage(mediaSource);
            entry.target.load(); // Recharge l'élément média
            self.unobserve(mediaSource);
          }
        }
      } else {
        // Modifie la data-src en src avec une fonction preloadImage()
        preloadImage(entry.target);
      }

      // L'image est chargée, l'API peut s'arrêter jusqu'à la prochaine, etc.
      self.unobserve(entry.target);
    }
  });
}, config);

// Sélectionne les images et lance l'observer asynchrone
const medias = document.querySelectorAll('[data-src]');
medias.forEach(function(media) {
  // Observation des images à charger au fur et à mesure
  observer.POLL_INTERVAL = 100;
  observer.observe(media);
});

// Instanciation de l'intersectionObserver pour le lazy loading
var backgroundObserver = new IntersectionObserver(function(entries, self) {
  // Pour chaque entrée ciblée (les images ici)
  entries.forEach(function(entry) {
    // L'API Javascript vérifie que l'entrée existe...
    if(entry.isIntersecting) {
      // Suppression de la class originelle
      entry.target.classList.remove("bckg-media");

      // Ajoute une classe visible pour afficher la bonne image
      entry.target.classList.add("visible");

      // L'image est chargée, l'API peut s'arrêter jusqu'à la prochaine, etc.
      self.unobserve(entry.target);
    }
  });
}, config);

// Même travail pour les images en background
var bckgMedias = document.querySelectorAll('.bckg-media');
bckgMedias.forEach(function(media) {
  // Observation des images à charger au fur et à mesure
  backgroundObserver.POLL_INTERVAL = 100;
  backgroundObserver.observe(media);
});