Rakshan Shetty

Native lazy loading image with React

February 22, 2020☕️ 2 min read

Native image lazy loading is added to browsers from Chrome 76 and other browsers are also starting to support it which helps us to add lazy image capabilities just by adding loading attribute.

Let’s create a basic react component that implements image lazy loading.

NativeLazyImage.js
import React from "react"
import PropTypes from "prop-types"

const NativeLazyImage = ({ src, alt, ...rest }) => {
  return <img src={src} alt={alt} {...rest} loading="lazy" />
}

NativeLazyImage.propTypes = {
  src: PropTypes.string.isRequired,
  alt: PropTypes.string.isRequired,
}

export default NativeLazyImage

That’s it, this is all we need to leverage browsers native image loading and does not have to deal with page scroll position manually to see if <img /> element in out of the viewport.

What about older browsers?

Currently, there are two ways to implement lazy loading offscreen images

Let’s build <LazyImage /> component which will use native loading attribute for newer browser and Intersection Observer for older browser.

LazyImage.js
import React from "react"
import PropTypes from "prop-types"
import IntersectionLazyImage from "./IntersectionLazyImage"
import NativeLazyImage from "./NativeLazyImage"

const LazyImage = props => {
  if ("loading" in HTMLImageElement.prototype) {
    return <NativeLazyImage {...props} />
  }
  return <IntersectionLazyImage {...props} />
}

LazyImage.propTypes = {
  src: PropTypes.string.isRequired,
  alt: PropTypes.string.isRequired,
}

export default LazyImage

We already created <NativeLazyImage/> above. Lets create <IntersectionLazyImage/>

IntersectionLazyImage.js
import React, { useState, useRef, useEffect } from "react"
import PropTypes from "prop-types"

import Observer from "./helpers/Observer" // will implement this shortly
IntersectionLazyImage

const observer = new Observer()

const IntersectionLazyImage = props => {
  const { src: imgSrc, lazysrc, alt, ...rest } = props
  const ref = useRef(null)
  const [src, setSrc] = useState(lazysrc)

  useEffect(() => {
    const r = ref.current
    const onIntersect = () => {
      setSrc(imgSrc)
    }
    observer.observe(r, onIntersect)
    return () => {
      observer.unobserve(r)
    }
  }, [imgSrc])

  return <img src={src} alt={alt} {...rest} imgRef={ref} />
}

IntersectionLazyImage.defaultProps = {
  lazysrc: "",
}

IntersectionLazyImage.propTypes = {
  lazysrc: PropTypes.string,
  src: PropTypes.string.isRequired,
  alt: PropTypes.string.isRequired,
}

export default IntersectionLazyImage

Finally, the Observer that will listen to all the images element and load image when visible inside the viewport

Observer.js
class Observer {
  elementMap = new Map()

  options = {
    threshold: 0,
    repeat: false,
  }

  obsvr = null

  constructor(options = {}) {
    this.init()
    this.options = { ...this.options, ...options }
  }

  init = async () => {
    this.obsvr = new IntersectionObserver(this.callBack, this.options)
  }

  callBack = entries => {
    entries.forEach(entry => {
      if (!entry.isIntersecting) return
      const callback = this.elementMap.get(entry.target)
      callback && callback()
      !this.options.repeat && this.unobserve(entry.target)
    })
  }

  observe = (target, onIntersect) => {
    this.elementMap.set(target, onIntersect)
    this.obsvr.observe(target)
  }

  unobserve = target => {
    this.obsvr.unobserve(target)
    this.elementMap.delete(target)
  }
}

export default Observer

Demo


Tags: Tutorials, React
Rakshan Shetty

Software engineer, Learning Web development and sharing my experience