Lazy loading images with Intersection Observer in React

Author Mileta Dulovic
Author

Mileta Dulovic

Let's see how we can make custom image components that will handle loading the image lazily and improve website performance significantly, by using IntersectionObserver. We will do this in React, but same logic can be used in any JavaScript framework.

Intersection Observer

If you don't know what Intersection Observer is here is the definition from MDN

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

In other words, you can detect when element intersects with other elements or the viewport (when it leaves or enters viewport).

Here is how we initialize Intersection Observer in JavaScript

const observer = new IntersectionObserver(onIntersection, {
  root: null, // default is the viewport
  threshold: 0.5, // percentage of target's visible area. Triggers "onIntersection"
});

As you can see root is null, and that is because in React we don't access elements directly in the DOM, but rather we use refs, like this.

<img
  {...props}
  ref={ref}
/>

Now that we assigned ref to the element we can connect it to Intersection Observer like this

observer.observe(ref.current);

Once we connect the element, onIntersection function will be triggered every time element enters the viewport. Function looks something like this:

function onIntersection(entries, opts) {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      // if element is in viewport do something
    }
  });
}

Okay, so now that you know what Intersection Observer is let's continue.

Adding placeholer images

Instead of loading image immediately on site load, we will add placeholder element (can be anything but we will use placeholder image) that will be used as a trigger for when it reaches viewport.

<img
  {...props}
  ref={placeholderRef}
  src="/placeholder.png"
  alt={props.alt || ""}
/>

Note that placeholder image has ref that we will pass to IntersectionObserver.

Final code

When we connect all the code pieces and logic from above we get this component that we can use as a drop-in <img /> replacement in our website.

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

const LazyImage = (props) => {
  const [inView, setInView] = useState(false);

  const placeholderRef = useRef();

  function onIntersection(entries, opts) {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        setInView(true);
      }
    });
  }

  useEffect(() => {
    const observer = new IntersectionObserver(onIntersection, {
      root: null, // default is the viewport
      threshold: 0.5, // percentage of target's visible area. Triggers "onIntersection"
    });

    if (placeholderRef?.current) {
      observer.observe(placeholderRef.current);
    }

    return () => {
      observer.disconnect();
    };
  }, []);

  return inView ? (
    <img {...props} alt={props.alt || ""} />
  ) : (
    <img
      {...props}
      ref={placeholderRef}
      src="/placeholder.png"
      alt={props.alt || ""}
    />
  );
};

export default LazyImage;

Conclusion

This is simple way to improve website performance, and provide our users with beter experience while browsing.

We will cover more ways to optimize and improve website performance in future posts, so stay tuned.