Charger des images lors du scroll avec l'IntersectionObserver

JavascriptTypescriptReactjs
Jérémie Loscos - 24/07/2018 à 15:38:360 commentaire


IntersectionObserver


IntersectionObserver (https://developer.mozilla.org/fr/docs/Web/API/Intersection_Observer_API) est une API javascript qui permet d'observer l'intersection entre une zone scrollable et des éléments qu'elle contient. Cela permet sur une page avec une barre de défilement de savoir lorsqu'un élément html devient visible pour l'utilisateur.


Cette API est très simple à utiliser, il suffit d'instancier un nouvel IntersectionObserver en lui passant une fonction de callback appelé lorsque l'intersection avec le éléments observés change:


var obs = new IntersectionObserver((elements) => {
  console.log(elements);
}, {
  root: null
  rootMargin: '50px 0px', //Applique une marge de 50px verticalement
  threshold: 0.01, //Seuil d'intersection des éléments (10%)
});
  


Paramètres du constructeur :

  • root indique le conteneur scrollable par rapport auquel on veut mesurer l'intersection. La valeur null (valeur par défaut) indique qu'on mesure l'intersection par rapport au viewport.
  • rootMargin est la marge autour du conteneur que l'on veut prendre en compte dans les calculs d'intersections. "50px 0px" applique une marge de 50px en haut et en bas du conteneur. Un élément sera donc considéré comme ayant une intersection s'il est à moins de 50px verticalement du viewport.
  • threshold indique le seuil d'intersection en pourcentage à partir duquel on veut que la fonction de callback s'exécute. Avec une valeur de 0.01, la fonction de callback sera déclenchée si au moins 10% de la surface d'un élément HTML a une intersection avec le viewport.



Exemple de pourcentages d'intersection d'éléments HTML avec le viewport



Une fois qu'on a instancié un observer on peut commencer à observer des éléments avec la méthode observe :


var imageTitre = document.getElementById("logo")

obs.observe(imageTitre); //Commence à observer les intersections avec l'élément
...
obs.unobserve(imageTitre); //Arrête d'observer les intersection avec cet élément



Il est bien plus optimal de réutiliser un IntersectionObserver pour observer plusieurs éléments plutôt que d'en instancier un pour chaque élément qu'on veut observer, et il faut bien penser à arrêter l'observation d'un élément dès qu'on en a plus besoin.


L'a fonction de callback est exécuté sur le thread principal avec une priorité basse. Si le navigateur a peu de ressources disponible, la fonction de callback peut donc être appelé bien après que le seuil d'intersection ai été franchis. L'IntersectionObserver n'est donc pas adapté pour gérer des animations ou autres utilisations nécessitant des temps de réponses précis.



Les images paresseuses


En développement Web, on cherche souvent à optimiser un maximum le temps de chargement de nos pages, pour ça une bonne solution et de faire un lazy loading des images de la page.

L'IntersectionObserver est très adapté pour faire du lazy loading d'image sur une page web. C'est à dire ne pas charger toutes les images d'une page à son chargement, mais uniquement lorsqu'elles deviennent visibles.


Pour cela il suffit de stocker l'url de l'image dans un attribut data, et de ne l'affecter à l'attribut src uniquement quand l'image a une intersection avec le viewport


function loadImage(img) {
    img.src = img.attributes["data-src"].value;
}

if ('IntersectionObserver' in window) {
    let obs = new IntersectionObserver((entries) => {
        entries.forEach(img => {
            loadImage(img.target);
            obs.unobserve(img.target);
        })
    }, {
        rootMargin: "15px 0px",
        threshold: 0.01
    })
    document.querySelectorAll(".lazy-image").forEach(img => obs.observe(img));
}
else {
    document.querySelectorAll(".lazy-image").forEach(img => loadImage(img));
}


Et dans le HTML on a :


<img class="lazy-image" src="/images/default-image.png" data-src="/images/real-image.png" height="150px"/>


Ici le rootMargin à 15px 0px, avec un threshold à 10% fait que lorsque l'utilisateur scroll verticalement la page, l'image de 150px de hauteur ne sera chargée que quand le bord de l'image touche le viewport. On peut ajuster le rootMargin et threshold pour commencer ce chargement plus tôt ou plus tard



Et en reactjs?


On a vu comment réaliser un lazy loading d'image en javascript pure, mais ce code ne marcherait pas directement avec un framework javascript comme reactjs.


Voici un exemple typescript d'une classe qui en plus d'instancier un IntersectionObserver va conserver un Map pour associer les éléments observés avec une fonction de callback spécifique


export class ElementObserver {
    private static  observedImages = new Map<Element, () => void>();

    private static Observer = 'IntersectionObserver' in window ? 
                                    new IntersectionObserver((entries) => {
                                        entries.forEach(entry => {
                                            if (entry.intersectionRatio > 0) {
                                                let elt = entry.target;
                                                ElementObserver.observedImages.get(elt)();
                                                ElementObserver.unobserve(elt);
                                            }
                                        });
                                    }, {
                                        rootMargin: '50px 0px',
                                        threshold: 0.01
                                    }) 
                                : null;

    public static observe(elt: Element, callback: () => void) {
        if (ElementObserver.Observer != null) {
            ElementObserver.Observer.observe(elt);
            ElementObserver.observedImages.set(elt, callback);
        } else {
            callback();
        }
    }

    public static unobserve(elt: Element) {
        if (ElementObserver.Observer != null) {
            ElementObserver.observedImages.delete(elt);
            ElementObserver.Observer.unobserve(elt);
        }
    }
}


En utilisant la classe ci-dessus, on peut facilement créer un composant react (en typescript) qui fait un lazy loading d'images :


export class LazyImage extends React.Component<{src:string, className?:string, style?:any}, {loaded:boolean}> {
    imgRef: React.RefObject<HTMLImageElement>;
    constructor(props) {
        super(props);
        this.state = {
            loaded:false
        };
        this.imgRef = React.createRef<HTMLImageElement>();
    }
    componentDidMount(){
        ElementObserver.observe(this.imgRef.current, this.onImgVisible.bind(this));
    }
    componentWillUnmount() {
        if (this.imgRef.current)
            ElementObserver.unobserve(this.imgRef.current);
    }
    onImgVisible() {
        this.setState({loaded:true});
    }
    render() {
        const defaultImage = "/images/default-image.png";
        return (<img ref={this.imgRef} style={this.props.style} className={this.props.className} src={this.state.loaded ? this.props.src : defaultImage}/>);
    }
}

ReactDOM.render(<LazyImage src="/images/logo.png" />, document.getElementById('root'));



Ça à l'air pratique, ça marche sous IE ?


La spec de l'API IntersectionObserver est encore en draft, mais la fonctionnalité est déjà supportée sur tous les navigateurs (Edge, Chrome, Safari, Firefox, Opera). Si notre chef nous demande de faire un site compatible avec IE, après 30 minutes à lui expliquer pourquoi on est pas content on peut utiliser les polyfills w3c pour IntersectionObserver




Commentaires :

Aucun commentaires pour le moment


Laissez un commentaire :

Réalisé par
Expaceo