Lazyload and Responsively Serve External Images In GatsbyJs

For the Gatsby version of my website, currently in development, I am serving all my images from Imagekit.io - a global image CDN. The reasons for doing this is so I will have the ultimate flexibility in how images are used within my site, which didn’t necessarily fit with what Gatsby has to offer especially when it came to how I wanted to position images within blog post content served from markdown files.

As I understand it, Gatsby Image has two methods of responsively resizing images:

  1. Fixed: Images that have a fixed width and height.
  2. Fluid: Images that stretch across a fluid container.

In my blog posts, I like to align my images (just take look at my post about my time in the Maldives) as it helps break the post up a bit. I won’t be able to achieve that look by the options provided in Gatsby. It’ll look all a little bit too stacked. The only option is to serve my images from Imagekit.io, which in the grand scheme isn’t a bad idea. I get the benefit of being able to transform images on the fly, optimisation (that can be customised through Imagekit.io dashboard) and fast delivery through its content-delivery network.

To meet my image requirements, I decided to develop a custom responsive image component that will perform the following:

  • Lazyload image when visible in viewport.
  • Ability to parse an array “srcset" sizes.
  • Set a default image width.
  • Render the image on page load in low resolution.

React Visibility Sensor

The component requires the use of "react-visibility-sensor” plugin to mimic the lazy loading functionality. The plugin notifies you when a component enters and exits the viewport. In our case, we only want the sensor to run once an image enters the viewport. By default, the sensor is always fired every time a block enters and exits the viewport, causing our image to constantly alternate between the small and large versions - something we don't want.

Thanks for a useful post by Mark Oskon, he provided a solution that extends upon the react-visibility-sensor plugin and allows us to turn off the sensor after the first reveal. I ported the code from Mark's solution in a newly created component housed in "/core/visibility-sensor.js", which I then reference into my LazyloadImage component:

import React, { Component } from "react";
import PropTypes from "prop-types";
import VSensor from "react-visibility-sensor";

class VisibilitySensor extends Component {
  state = {
    active: true
  };

  render() {
    const { active } = this.state;
    const { once, children, ...theRest } = this.props;
    return (
      <VSensor
        active={active}
        onChange={isVisible =>
          once &&
          isVisible &&
          this.setState({ active: false })
        }
        {...theRest}
      >
        {({ isVisible }) => children({ isVisible })}
      </VSensor>
    );
  }
}

VisibilitySensor.propTypes = {
  once: PropTypes.bool,
  children: PropTypes.func.isRequired
};

VisibilitySensor.defaultProps = {
  once: false
};

export default VisibilitySensor;

LazyloadImage Component

import PropTypes from "prop-types";
import React, { Component } from "react";
import VisibilitySensor from "../core/visibility-sensor"

class LazyloadImage extends Component {
    render() {
      let srcSetAttributeValue = "";
      let sanitiseImageSrc = this.props.src.replace(" ", "%20");

      // Iterate through the array of values from the "srcsetSizes" array property.
      if (this.props.srcsetSizes !== undefined && this.props.srcsetSizes.length > 0) {
        for (let i = 0; i < this.props.srcsetSizes.length; i++) {
          srcSetAttributeValue += `${sanitiseImageSrc}?tr=w-${this.props.srcsetSizes[i].imageWidth} ${this.props.srcsetSizes[i].viewPortWidth}w`;

          if (this.props.srcsetSizes.length - 1 !== i) {
            srcSetAttributeValue += ", ";
          }
        }
      }

      return (
          <VisibilitySensor key={sanitiseImageSrc} delayedCall={true} partialVisibility={true} once>
            {({isVisible}) =>
            <>
              {isVisible ? 
                <img src={`${sanitiseImageSrc}?tr=w-${this.props.widthPx}`} 
                      alt={this.props.alt}
                      sizes={this.props.sizes}
                      srcSet={srcSetAttributeValue} /> : 
                <img src={`${sanitiseImageSrc}?tr=w-${this.props.defaultWidthPx}`} 
                      alt={this.props.alt} />}
              </>
            }
          </VisibilitySensor>
      )
    }
}

LazyloadImage.propTypes = {
  alt: PropTypes.string,
  defaultWidthPx: PropTypes.number,
  sizes: PropTypes.string,
  src: PropTypes.string,
  srcsetSizes: PropTypes.arrayOf(
    PropTypes.shape({
      imageWidth: PropTypes.number,
      viewPortWidth: PropTypes.number
    })
  ),
  widthPx: PropTypes.number
}

LazyloadImage.defaultProps = {
  alt: ``,
  defaultWidthPx: 50,
  sizes: `50vw`,
  src: ``,
  widthPx: 50
}

export default LazyloadImage

Component In Use

The example below shows the LazyloadImage component used to serve a logo that will serve a different sized image with the following widths - 400, 300 and 200.

<LazyloadImage 
                src="https://ik.imagekit.io/surinderbhomra/Pages/logo-me.jpg" 
                widthPx={400} 
                srcsetSizes={[{ imageWidth: 400, viewPortWidth: 992 }, { imageWidth: 300, viewPortWidth: 768 }, { imageWidth: 200, viewPortWidth: 500 }]}
                alt="Surinder Logo" />

Useful Links

https://alligator.io/react/components-viewport-react-visibility-sensor/ https://imagekit.io/blog/lazy-loading-images-complete-guide/ https://www.sitepoint.com/how-to-build-responsive-images-with-srcset/