Skip to content

Instantly share code, notes, and snippets.

@akramsaouri
Last active January 31, 2023 23:24
Show Gist options
  • Save akramsaouri/533b90326a811ed7adb75062e069b6a9 to your computer and use it in GitHub Desktop.
Save akramsaouri/533b90326a811ed7adb75062e069b6a9 to your computer and use it in GitHub Desktop.
⚛️ Lazy load images in React with IntersectionObserver.
import React, { useEffect, useRef, FunctionComponent, HTMLProps } from 'react';
import 'intersection-observer'; // if you want to include a polyfill
/**
* Returns an IntersectionObserver that loads the image
* when it is at least {threshold}*100 visible in the viewport.
*
* PS: Cached on the window for performance
*/
function getImageLoaderObserver(
ioOptions: IntersectionObserverInit = {},
): null | IntersectionObserver {
const { threshold = 0.01 } = ioOptions;
// cache key is dependent of io args
const CACHE_KEY = `__LAZY_IMAGE_IO_${JSON.stringify(ioOptions)}`;
if (typeof IntersectionObserver !== 'function') {
return null;
}
// return the cached observer for performance
if (typeof window[CACHE_KEY] !== 'undefined') {
return window[CACHE_KEY];
}
// create a new observer and cache it on the window
window[CACHE_KEY] = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
const dataSrc = img.getAttribute('data-src');
if (dataSrc) {
const parent = img.parentNode as HTMLElement;
if (parent?.nodeName === 'PICTURE') {
// handles special <picture><source> case
Array.from(parent.getElementsByTagName('source')).forEach(
(source: HTMLSourceElement) => {
const dataSrcSet = source.getAttribute('data-srcset');
if (!img.hasAttribute('srcset') && dataSrcSet) {
source.setAttribute('srcset', dataSrcSet);
}
},
);
}
if (!img.hasAttribute('src') || dataSrc !== img.getAttribute('src')) {
img.setAttribute('src', dataSrc);
}
}
}
});
},
{
...ioOptions,
threshold,
},
);
return window[CACHE_KEY];
}
export interface LazyImageProps extends HTMLProps<HTMLImageElement> {
className?: string;
src: string;
alt?: string;
errorSrc?: string;
ioOptions?: IntersectionObserverInit;
onError?: () => void;
onClick?: (e: any) => void;
}
const LazyImage: FunctionComponent<LazyImageProps> = ({
className,
src,
alt,
ioOptions = {},
errorSrc,
onError,
onClick,
style,
}: LazyImageProps) => {
const imgRef = useRef<HTMLImageElement | null>(null);
const handleError = () => {
// manually attach fallback src to img on Error
if (imgRef.current && errorSrc) {
imgRef.current.src = errorSrc;
onError && onError();
}
};
useEffect(() => {
const observer = getImageLoaderObserver(ioOptions);
const target = imgRef.current;
if (observer && target) {
observer.observe(target);
}
return () => {
if (observer && target) {
observer.unobserve(target);
}
};
}, [src]);
return (
<img
className={className}
data-src={src}
ref={imgRef}
onError={handleError}
alt={alt}
onClick={onClick}
style={style}
/>
);
};
export default LazyImage;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment