← Back to overview

Lazy load images using IntersactionObserver API

Publish date: 07 April 2024

If you want to lazy load an image or have a fade-in animation when things are scrolled into view you could listen to the scroll-event and do all the calculations yourself. But this is a huge pain in the ass. Since 2018 we can use the Intersection Observer API which makes this much easier.

Here is a simple example with React and TypeScript:

import { useEffect, useRef, useState } from "react";

export type LazyImageProps = {
  src: string;
  alt: string;
};

export const LazyImage = ({ src, alt }: LazyImageProps) => {
  const imgRef = useRef<HTMLElement | null>(null);
  const [showImg, setShowImg] = useState(false);

  useEffect(() => {
    const onObserve = (
      entries: IntersectionObserverEntry[],
      observer: IntersectionObserver
    ) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          setShowImg(true);
          imgRef.current && observer.unobserve(imgRef.current);
        }
      });
    };

    const observer = new IntersectionObserver(onObserve);
    imgRef.current && observer.observe(imgRef.current);

    return () => observer.disconnect();
  });

  return (
    <img
      ref={(ref) => (imgRef.current = ref)}
      src={showImg ? src : ""}
      alt={alt}
    />
  );
};

Here I am setting the src-attribute when the image gets scrolled into view, but you could also set a className that triggers a CSS animation.

Want to trigger it before it is completely scrolled into view, or even right before it becomes visible? Set some options:

const options: IntersectionObserverInit = {
  rootMargin: "100px",
  threshold: [0, 0.25, 0.5, 1],
};

// pass the object to the constructor of IntersectionObserver
const observer = new IntersectionObserver(onObserve, options);

rootMargin sets a margin of 100px around the image, and with threshold you can make it trigger multiple times. 1 means when the image is 100% visible, 0.5 means only half of the image is visible.