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" 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