Chargement asynchrone des images et fallback

Le chargement asynchrone des images en HTML fait encore office de science fiction, pour le moment si vous voulez charger en "lazy loading" des images vous devez vous reposer sur un peu de JavaScript.

Il y a déjà une tonne de guides pour vous dire comment faire. Encore une autre tonne de librairies pour vous aider à le faire. Mais la plupart de ce que j'ai pu voir a l'un de ces deux gros défaut :

Pour des raisons de facilité, de lisibilité et de performance je vais utiliser un Intersection Observer. Cette techno n'est supportée que depuis quelques jours sur Safari Desktop et sera disponible très prochainement sous iOS. Bien sûr aucun support du côté d'IE. Pas de panique, les navigateurs n'ayant pas connaissance des Intersection Observer recevront quand même les images, sans le lazy loading. Pour plus de détails sur le support, voir la page CanIUse correspondante.

Nul de besoin de bien connaître l'API des Intersections Observers pour appréhender le code ci dessous, vous avez juste à comprendre que cela va nous permettre de savoir quand un élément est presque visible.

Afficher les images uniquement pour ceux n'ayant pas de JavaScript

L'idée n'est pas très élaborée, nous allons mettre nos balises images dans des balises <noscript></noscript>, de cette manière elle ne seront affichées que pour les users agents1 sans JavaScript.

<h1>Voici le haut de page</h1>

<noscript>
	<img src="/top.png" alt="Bonjour visiteur">
</noscript>

<h2 style="margin-top:150vh">Voici le bas de la page</h2>

<noscript>
	<img src="/bot.png" alt="bienvenue dans les abysses">
</noscript>

<noscript>
	<img src="/last.png" alt="aurevoir">
</noscript>

Pour l'exemple j'ai mis une très grosse marge au dessus de mon h2.

Ma première idée était de déplacer la balise des images en dehors des balises noscript quand on se rapproche de leur position, mais les balises noscript sont totalement ignorées par les navigateurs avec du JS et ne seront jamais "attrapées" par les Intersections Observers. La solution que j'ai choisi est de mettre chaque balises noscript dans une div, ainsi celle-ci sera prise en compte par le navigateur tout en restant invisible car elle a une hauteur nulle.

<h1>Voici le haut de page</h1>

<div class="lazyimgtag">
    <noscript>
        <img src="/top.png" alt="Bonjour visiteur">
    </noscript>
</div>

<h2 style="margin-top:150vh">Voici le bas de la page</h2>

<div class="lazyimgtag">
	<noscript>
		<img src="/bot.png" alt="bienvenue dans les abysses">
	</noscript>
</div>

<div class="lazyimgtag">
	<noscript>
		<img src="/last.png" alt="aurevoir">
	</noscript>
</div>

Afficher nos images le moment opportun pour ceux qui ont JavaScript

Nous écrivons notre Intersection Observer pour nous dire quand la position de l'image est visible, et cela avec une petite marge, ici de 80 pixels. Une fois l'image chargée nous n'avons plus besoin de vérifier si sa position est visible, nous arrêtons donc l'Intersection Observer pour celle-ci.

let observer = new IntersectionObserver(function (entries, observer) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                // La position de l'image + la marge est visible
                
                // L'image est chargée, nous arrêtons de surveiller sa div .lazyimgtag parente
                observer.unobserve(entry.target);
            }
        });
    }, {rootMargin: '55px'}
);

Pour charger l'image nous insérons juste après le noscript son propre contenu

// où 'div' est une de nos div avec la classe lazyimgtag
div.insertAdjacentHTML('afterend', div.querySelectorAll('noscript')[0].textContent);

Et voici comment nous observons chacune de nos divs .lazyimgtag

// Nous commençons à observer toutes les divs avec la classe lazyimgtag
document.querySelectorAll('div.lazyimgtag').forEach(image => {
    observer.observe(image);
});

Afficher normalement les images sur les navigateurs ne supportant pas IntersectionObserver

Sur les navigateurs ne supportant pas IntersectionObserver, nous allons charger toutes les images pendant l’exécution du JavaScript.

Cela nécessite de savoir si la fonction existe

typeof IntersectionObserver === "function"

Si ce n'est pas le cas, plutôt que d'observer chacune des divs avec la classe lazyimgtag, nous chargeons d'un coup toutes les images

document.querySelectorAll('div.lazyimgtag').forEach(function (image) {
    // ici la copie du contenu des noscripts à l'exterieur de ceux-ci
});

Pour conclure, le JavaScript complet

lazyImgTag: function () {
    const
        images = document.querySelectorAll('div.lazyimgtag'),
        loadImage = function (image) {
        	// Copie du contenu des noscripts à l'extérieur de ceux-ci
            image.insertAdjacentHTML('afterend', image.querySelectorAll('noscript')[0].textContent)
        };

    if (typeof IntersectionObserver === "function") {
        let observer = new IntersectionObserver(function (entries, observer) {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        // La position de l'image + la marge est visible
                        loadImage(entry.target);
                        // L'image est chargée, nous arrêtons de surveiller sa div .lazyimgtag parente
                        observer.unobserve(entry.target);
                    }
                });
            }, {rootMargin: '55px'}
        );

        // Nous commençons à observer toutes les divs avec la classe lazyimgtag
        images.forEach(function (image) {
            observer.observe(image);
        });
    } else {
        // Si les IntersectionObserver ne sont pas supportés, nous chargeons toutes les images
        images.forEach(function (image) {
            loadImage(image);
        });
    }
}

Illustration (libre d'usage) par Colin Watts

Si vous voyez des façons d’améliorer quelque-chose ou que vous voulez réagir à cet article, n'hésitez pas à m'interpellez où à en discuter avec moi ☺ !

  1. "Les agents utilisateur du Web vont de la gamme des navigateurs jusqu'aux robots d'indexation, en passant par les lecteurs d'écran ou les navigateurs braille pour les personnes ayant une incapacité. " – Wikipedia