refactor image to only render in viewport once

This commit is contained in:
jeffvli
2026-01-10 14:22:05 -08:00
parent 89d4698155
commit 746951b55f
+57 -30
View File
@@ -6,6 +6,8 @@ import {
type ImgHTMLAttributes, type ImgHTMLAttributes,
memo, memo,
ReactNode, ReactNode,
useEffect,
useState,
} from 'react'; } from 'react';
import { Img } from 'react-image'; import { Img } from 'react-image';
@@ -43,6 +45,25 @@ interface ImageUnloaderProps {
export const FALLBACK_SVG = export const FALLBACK_SVG =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxwYXRoIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iLjA1IiBkPSJNMCAwaDMwMHYzMDBIMHoiLz48L3N2Zz4='; 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxwYXRoIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iLjA1IiBkPSJNMCAwaDMwMHYzMDBIMHoiLz48L3N2Zz4=';
interface ImageViewportWrapperProps {
children: (shouldRenderImage: boolean, ref: ForwardedRef<HTMLDivElement>) => ReactNode;
}
const ImageViewportWrapper = ({ children }: ImageViewportWrapperProps) => {
const [hasEnteredViewport, setHasEnteredViewport] = useState(false);
const { inViewport, ref } = useInViewport();
useEffect(() => {
if (inViewport && !hasEnteredViewport) {
setHasEnteredViewport(true);
}
}, [inViewport, hasEnteredViewport]);
const shouldRenderImage = hasEnteredViewport || inViewport;
return <>{children(shouldRenderImage, ref)}</>;
};
export function BaseImage({ export function BaseImage({
className, className,
containerClassName, containerClassName,
@@ -54,38 +75,44 @@ export function BaseImage({
unloaderIcon = 'emptyImage', unloaderIcon = 'emptyImage',
...props ...props
}: ImageProps) { }: ImageProps) {
const { inViewport, ref } = useInViewport();
return ( return (
<ImageContainer <ImageViewportWrapper>
className={containerClassName} {(shouldRenderImage, viewportRef) => {
enableAnimation={enableAnimation} return (
ref={ref} <ImageContainer
{...imageContainerProps} className={containerClassName}
> enableAnimation={enableAnimation}
{inViewport && src ? ( ref={viewportRef}
<Img {...imageContainerProps}
className={clsx(styles.image, className, { >
[styles.animated]: enableAnimation, {shouldRenderImage && src ? (
})} <Img
decoding="async" className={clsx(styles.image, className, {
fetchPriority="high" [styles.animated]: enableAnimation,
loader={includeLoader ? <ImageLoader className={className} /> : null} })}
loading="eager" decoding="async"
src={src} fetchPriority="high"
unloader={ loader={
includeUnloader ? ( includeLoader ? <ImageLoader className={className} /> : null
}
loading="eager"
src={src}
unloader={
includeUnloader ? (
<ImageUnloader className={className} icon={unloaderIcon} />
) : null
}
{...props}
/>
) : !src ? (
<ImageUnloader className={className} icon={unloaderIcon} /> <ImageUnloader className={className} icon={unloaderIcon} />
) : null ) : (
} <ImageLoader className={className} />
{...props} )}
/> </ImageContainer>
) : !src ? ( );
<ImageUnloader className={className} icon={unloaderIcon} /> }}
) : ( </ImageViewportWrapper>
<ImageLoader className={className} />
)}
</ImageContainer>
); );
} }