Mileta Dulovic
HomeBlogReact

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.

More in this category

JavaScript Loops performance

Yet another post that iterates over items to measure performance of different loops in JavaScript.

How to cancel requests with Apollo GraphQL

If you need to cancel requests with Apollo Graphql in React, and do not know how to do it, you came to the right place.

Infinite scroll component in React and NextJS

Infinite scroll is a great way to load huge lists as it provides a great UX for user when compared to traditional pagination

React: Pitch & anti-pitch

Weighing the Advantages and Disadvantages of React for Modern Web Development.