Go to Hackademy website

Images, Lazy loading ou Chargement paresseux

Victor Darras

Posté par dans la catégorie intégration

Bonjour à tous, aujourd’hui un sujet à cheval entre intégration et développement front, nous allons faire en sorte d’améliorer les performances de rendu de vos pages web. Je pense pouvoir avancer sans trop de risque qu’en général une page web est bien plus alourdie par ses images que par ses CSS ou JS. Il nous est tous déjà arrivé d’avoir besoin d’afficher un nombre important d’images sur une même page et c’est là que les choses se corsent.

Tant en termes de SEO que pour l’accessibilité (coucou toi avec ta 3G en rade), il est vite primordial de réduire le temps de chargement de vos pages. Pour cela, il existe plusieurs techniques complémentaires telles que :

  • la réduction des fichiers textes ;
  • la compression des images (je vous conseille le très bon ImageOptim) ;
  • l’insertion inline de CSS indispensables à un premier chargement de page ;
  • ou encore le chargement dynamique d’image sur lequel je vais m’attarder aujourd’hui.

Nous allons voir plusieurs techniques, chacune ayant ses bénéfices et ses défauts ; donc ne vous arrêtez pas à la première, qui pourrait ne pas être la plus performante (qui sait ?).

Côté donne, qu’est-ce que ça DOM ?

Pour nous simplifier la tâche, je vous propose de commencer par l’élément img adapté à notre défi. Dans le src mettons un placeholder pouvant être une image très légère permettant de remplir l’espace en attendant plus d’infos, ou une data-url en base64 qui permettra d’éviter une requête HTTP supplémentaire. Ce src étant temporaire, nous aurons besoin de l’URL réelle. Pourquoi pas définir un attribut data-src que nous utiliserons à la volée ? On va aussi lui administrer un peu de classe pour pouvoir la retrouver plus facilement parmi les autres images chargées de manière standard (pour les plus attentifs, nous pourrions aussi utiliser un sélecteur comme [data-src]).

<img
  src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
  data-src="/url-image.png"
  class="lazy"
  alt="Description"
  />

À part si vous utilisez un placeholder de la même taille que votre image finale (une version floue à la Medium par exemple), je vous conseille à cette étape d’ajouter des dimensions à votre image — avec des attributs ou en CSS — pour éviter de probables effets de redimensionnement de page.

Maintenant passons aux choses sérieuses…

Quelques bases avec JavaScript

Voyons dans un premier temps comment implémenter de manière basique notre solution. Demandons-nous si l’image que l’on veut charger est bien visible à l’écran avec la méthode isImgVisible. Celle-ci compare la distance entre le bord haut de l’image et le haut de la fenêtre de l’utilisateur, si elle est nulle ou négative, cette image devrait être visible.

function isImgVisible(img) {
  return img.getBoundingClientRect().top <= window.innerHeight;
}

Ensuite nous sélectionnons l’ensemble des images ayant la classe lazy définie précédemment. La fonction showImgs() passera sur chacune des images et lorsque l’une d’entre elles sera visible, l’attribut src sera mis à jour avec l’URL de l’image recherchée.

const imgs = document.querySelectorAll("img.lazy");

function showImgs() {
  imgs.forEach(function(img) {
    if (isImgVisible(img)){
      img.src = img.dataset.src;
    }
  });
}

Enfin on vérifie régulièrement si les images doivent être visibles ou non :

window.setInterval(showImgs, 1000)

Beaucoup de défauts à cette solution :

  • On ne supprime pas l’interval, et plus le nombre d’images est grand, moins ça fonctionne ;
  • la méthode getBoundingClientRect appelée trop souvent peut créer des effets de ralentissement ;
  • on réassigne les sources de toutes les images, même celles déjà chargées ;
  • on ne gère pas les différentes sources possibles, comme on pourrait le faire avec l’attribut srcset ou le duo picture/source.

Elle me permet néanmoins de poser des bases de logique à améliorer par la suite.

La solution la plus compatible

Quelques points d’amélioration notables pour cette seconde version :

Node loop

querySelectorAll().forEach() n’est pas compatible avec Safari ou IE/Edge, l’utilisation de Array.prototype.slice.call() permet de transformer un ensemble de NODE en un tableau standard (plus d’infos)

var lazyImages = Array.prototype.slice.call(document.querySelectorAll("img.lazy"));

setTimeout

Une variable (flag) active et un setTimeout pour empêcher d’exécuter la fonction plus souvent que toutes les 200ms (aussi appelé throttling) et réduire ainsi les risques de ralentissement. Avec ES6, on peut aussi utiliser Array.from(), plus explicite.

var active = false;

var lazyLoad = function() {
  if (!active) {
    active = true;
    setTimeout(function() {
      // Actions
      active = false;
    }, 200);
  }
}

L’image est-elle visible ?

La fonction isImgVisible est ici plus complète et vérifie que le haut et le bas de l’image sont bien dans la fenêtre, et si l’image n’a pas de style display:none.

var isImgVisible = function (lazyImage) {
  return lazyImage.getBoundingClientRect().top <= window.innerHeight
    && lazyImage.getBoundingClientRect().bottom >= 0
    && getComputedStyle(lazyImage).display !== "none"
}

Gestion de sources

L’utilisation de data-src est toujours la même, si ce n’est qu’on lui ajoute une gestion de srcset dans le cas de sources multiple, pour la gestion d’écrans Retina notamment.

lazyImage.src = lazyImage.dataset.src;
lazyImage.srcset = lazyImage.dataset.srcset;

Réduire le tableau d’images

À chaque occurrence nous réduisons l’Array contenant notre liste d’images, ainsi la boucle est de plus en plus courte, et on économise de la mémoire.

lazyImages = lazyImages.filter(function(image) {
  return image !== lazyImage;
});

Activer le chargement dynamique

Pour finaliser ce script il ne nous reste qu’à activer notre fonction. Puisqu’elle n’est plus exécutée au lancement de la page comme dans le script précédent, nous allons l’attacher aux événements scroll, resize et orientationchange (pour mobile).

window.addEventListener("scroll", lazyLoad);
window.addEventListener("resize", lazyLoad);
window.addEventListener("orientationchange", lazyLoad);

Toutes les images sont chargées, on détruit tout

En fin de boucle, on prend le temps de vérifier s’il reste des images à afficher. Dans le cas contraire, on supprime l’ensemble des EventListener associés.

if (lazyImages.length === 0) {
  window.removeEventListener("scroll", lazyLoad);
  window.removeEventListener("resize", lazyLoad);
  window.removeEventListener("orientationchange", lazyLoad);
}

Le script au complet

Maintenant que nous avons vu chaque détail d’amélioration, voici la version complète :

var lazyImages = Array.prototype.slice.call(document.querySelectorAll("img.lazy"));
var active = false;

var isImgVisible = function (lazyImage) {
  return lazyImage.getBoundingClientRect().top <= window.innerHeight
    && lazyImage.getBoundingClientRect().bottom >= 0
    && getComputedStyle(lazyImage).display !== "none"
}

var lazyLoad = function() {
  if (!active) {
    active = true;

    setTimeout(function() {
      lazyImages.forEach(function(lazyImage) {
        if (isImgVisible(lazyImage)) {
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;

          lazyImages = lazyImages.filter(function(image) {
            return image !== lazyImage;
          });

          if (lazyImages.length === 0) {
            window.removeEventListener("scroll", lazyLoad);
            window.removeEventListener("resize", lazyLoad);
            window.removeEventListener("orientationchange", lazyLoad);
          }
        }
      });

      active = false;
    }, 200);
  }
};

window.addEventListener("scroll", lazyLoad);
window.addEventListener("resize", lazyLoad);
window.addEventListener("orientationchange", lazyLoad);

Avec cette solution plus évoluée, vous prendrez en compte un maximum de navigateurs (IE9+), mais les performances ne seront toujours pas les meilleures que vous puissiez avoir.

Voyons maintenant comment rendre tout ça blazing fast.

Avec IntersectionObserver, plus de performance et plus de bonheur !

Grâce à l’API IntersectionObserver nous pouvons ajouter un observer asynchrone aux images que nous voulons charger. Celui-ci nous fournit des détails sur les intersections entre éléments, parents, et le document. Dans un premier temps, nous vérifions la disponibilité de l’API avec ("IntersectionObserver" in window). Nous créons ensuite un nouvel objet IntersectionObserver qui prend pour premier argument le callback qui modifiera les attributs de l’élément img comme vu précédemment.

Ce nouveau callback nous donne les différentes infos et états de l’observer :

IntersectionObserverEntry: [
  boundingClientRect: {x: 1327, y: 709, width: 436, height: 436, top: 709, },
  intersectionRatio: 1,
  intersectionRect: {x: 1327, y: 709, width: 436, height: 436, top: 709, },
  isIntersecting: true,
  rootBounds: {x: -50, y: -50, width: 2645, height: 1405, top: -50, },
  target: img.lazy,
  time: 559.4999999993888,
]

Nous n’aurons besoin que de isIntersecting pour cet exercice qui, une fois vérifié, nous permet de désactiver l’observer avec unobserve. Enfin nous pouvons activer l’observer pour chacune de nos images.

const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

if ("IntersectionObserver" in window) {
  const lazyImageObserver = new IntersectionObserver(function(entries) {
    entries.forEach(function(entry) {
      if (entry.isIntersecting) {
        const lazyImage = entry.target;
        lazyImage.src = lazyImage.dataset.src;
        lazyImage.srcset = lazyImage.dataset.srcset;
        lazyImageObserver.unobserve(lazyImage);
      }
    });
  });

  lazyImages.forEach(function(lazyImage) {
    lazyImageObserver.observe(lazyImage);
  });
} else {
  // Possibly fall back to a more compatible method here
}

En fallback, on pourrait utiliser la solution précédente, mais il semblerait que l’utilisation d’un polyfill soit une bonne idée aussi.

Nous voici avec une solution clé-en-main pour charger dynamiquement nos images, ou pourquoi pas mettre en place un infinite scrolling efficace !

Petits bonus

Il peut vous arriver de devoir définir un élément scrollable différent de body (par défaut), ou encore de vouloir prendre de l’avance sur le scroll de l’utilisateur, ou même de n’afficher l’image que lorsqu’elle est affichée d’au moins 25%. Pour cela, il suffit d’ajouter des options à l’instanciation de l’objet IntersectionObserver :

const options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}
const lazyImageObserver = new IntersectionObserver(callback, options);

Conclusion

Maintenant que nous avons fait le tour, j’espère que cette déclinaison étape par étape était claire et vous a plu. Dorénavant nous savons faire travailler le navigateur pour le rendre plus paresseux (avouez que c’est cocasse) ! Le lazy-loading c’est bon pour vos visiteurs, bon pour le référencement, c’est même bon pour la planète. Mangez-en !


L’équipe Synbioz.
Libres d’être ensemble.

Articles connexes

Servir rapidement ses images avec nginx et dragonfly

27/02/2015

Lorsqu’on utilise la gem dragonfly, le chargement des images de notre application ressemble à cela : I, [2015-02-27T16:16:32.957778 #3191] INFO -- : DRAGONFLY: GET /media/W1siZiIsIjIwMTUvMDIvMjEvOWoxOXhscmMzMV9NX2VuZmxhbW1lX2xlc19jZXNhcl8yMDE1XzA…

Comment remplacer une image en erreur

10/01/2017

Bonjour à tous, aujourd’hui un article très abordable puisque nous allons voir ensemble comment mettre en place un élément de remplacement pour les images afin d’éviter le fameux : Que ce soit pour intégrer cette erreur dans la charte graphique d…

Afficher plus Afficher moins

Ajouter un commentaire