Cet article est publié sous licence CC BY-NC-SA
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 :
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 ?).
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…
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 :
getBoundingClientRect
appelée trop souvent peut créer des effets de ralentissement ;srcset
ou le duo picture
/source
.Elle me permet néanmoins de poser des bases de logique à améliorer par la suite.
Quelques points d’amélioration notables pour cette seconde version :
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"));
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);
}
}
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"
}
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;
À 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;
});
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);
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);
}
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.
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 !
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);
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.
Nos conseils et ressources pour vos développements produit.