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.
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
- Using Intersection Observer API
- Using
scroll
event handlers
Let’s build <LazyImage />
component which will use native loading
attribute for newer browser and Intersection Observer for older browser.
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/>
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
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