track image viewport state in sessionStorage

This commit is contained in:
jeffvli
2026-02-01 18:55:04 -08:00
parent 216353837c
commit 7615c0d2ba
+67 -9
View File
@@ -26,7 +26,7 @@ export interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 's
imageContainerProps?: Omit<ImageContainerProps, 'children'>; imageContainerProps?: Omit<ImageContainerProps, 'children'>;
includeLoader?: boolean; includeLoader?: boolean;
includeUnloader?: boolean; includeUnloader?: boolean;
src: string | string[] | undefined; src: string | undefined;
thumbHash?: string; thumbHash?: string;
unloaderIcon?: keyof typeof AppIcon; unloaderIcon?: keyof typeof AppIcon;
} }
@@ -147,20 +147,31 @@ function ImageWithDebounce({
const hasBeenInViewportRef = useRef(false); const hasBeenInViewportRef = useRef(false);
const prevDebouncedSrcRef = useRef(debouncedSrc); const prevDebouncedSrcRef = useRef(debouncedSrc);
const srcInDisplayedCache = isInDisplayedCache(src);
if (srcInDisplayedCache) {
hasBeenInViewportRef.current = true;
}
if (prevDebouncedSrcRef.current !== debouncedSrc) { if (prevDebouncedSrcRef.current !== debouncedSrc) {
prevDebouncedSrcRef.current = debouncedSrc; prevDebouncedSrcRef.current = debouncedSrc;
hasBeenInViewportRef.current = false; if (!srcInDisplayedCache) hasBeenInViewportRef.current = false;
} }
if (inViewport && debouncedSrc) { if (inViewport && debouncedSrc) {
hasBeenInViewportRef.current = true; hasBeenInViewportRef.current = true;
} }
const effectiveSrc = debouncedSrc ?? (srcInDisplayedCache ? src : undefined);
const shouldShowImage = enableViewport const shouldShowImage = enableViewport
? (inViewport || hasBeenInViewportRef.current) && debouncedSrc ? (inViewport || hasBeenInViewportRef.current) && effectiveSrc
: debouncedSrc; : effectiveSrc;
if (enableViewport) { if (enableViewport) {
if (shouldShowImage && effectiveSrc) {
addToDisplayedCache(effectiveSrc);
}
return ( return (
<ImageContainer <ImageContainer
className={clsx(containerClassName, containerPropsClassName)} className={clsx(containerClassName, containerPropsClassName)}
@@ -168,7 +179,7 @@ function ImageWithDebounce({
ref={ref} ref={ref}
{...restContainerProps} {...restContainerProps}
> >
{shouldShowImage && debouncedSrc ? ( {shouldShowImage && effectiveSrc ? (
<Img <Img
className={clsx(styles.image, className, { className={clsx(styles.image, className, {
[styles.animated]: enableAnimation, [styles.animated]: enableAnimation,
@@ -176,7 +187,7 @@ function ImageWithDebounce({
decoding="async" decoding="async"
fetchPriority={fetchPriority} fetchPriority={fetchPriority}
loader={includeLoader ? <ImageLoader className={className} /> : null} loader={includeLoader ? <ImageLoader className={className} /> : null}
src={debouncedSrc} src={effectiveSrc}
unloader={ unloader={
includeUnloader ? ( includeUnloader ? (
<ImageUnloader className={className} icon={unloaderIcon} /> <ImageUnloader className={className} icon={unloaderIcon} />
@@ -193,13 +204,14 @@ function ImageWithDebounce({
); );
} }
if (effectiveSrc) addToDisplayedCache(effectiveSrc);
return ( return (
<ImageContainer <ImageContainer
className={clsx(containerClassName, containerPropsClassName)} className={clsx(containerClassName, containerPropsClassName)}
enableAnimation={enableAnimation} enableAnimation={enableAnimation}
{...restContainerProps} {...restContainerProps}
> >
{debouncedSrc ? ( {effectiveSrc ? (
<Img <Img
className={clsx(styles.image, className, { className={clsx(styles.image, className, {
[styles.animated]: enableAnimation, [styles.animated]: enableAnimation,
@@ -207,7 +219,7 @@ function ImageWithDebounce({
decoding="async" decoding="async"
fetchPriority={fetchPriority} fetchPriority={fetchPriority}
loader={includeLoader ? <ImageLoader className={className} /> : null} loader={includeLoader ? <ImageLoader className={className} /> : null}
src={debouncedSrc} src={effectiveSrc}
unloader={ unloader={
includeUnloader ? ( includeUnloader ? (
<ImageUnloader className={className} icon={unloaderIcon} /> <ImageUnloader className={className} icon={unloaderIcon} />
@@ -242,9 +254,14 @@ function ImageWithViewport({
const hasBeenInViewportRef = useRef(false); const hasBeenInViewportRef = useRef(false);
const prevSrcRef = useRef(src); const prevSrcRef = useRef(src);
const srcInDisplayedCache = isInDisplayedCache(src);
if (srcInDisplayedCache) {
hasBeenInViewportRef.current = true;
}
if (prevSrcRef.current !== src) { if (prevSrcRef.current !== src) {
prevSrcRef.current = src; prevSrcRef.current = src;
hasBeenInViewportRef.current = false; if (!srcInDisplayedCache) hasBeenInViewportRef.current = false;
} }
if (inViewport && src) { if (inViewport && src) {
@@ -253,6 +270,7 @@ function ImageWithViewport({
const shouldShowImage = (inViewport || hasBeenInViewportRef.current) && src; const shouldShowImage = (inViewport || hasBeenInViewportRef.current) && src;
if (shouldShowImage && src) addToDisplayedCache(src);
return ( return (
<ImageContainer <ImageContainer
className={clsx(containerClassName, containerPropsClassName)} className={clsx(containerClassName, containerPropsClassName)}
@@ -285,6 +303,46 @@ function ImageWithViewport({
); );
} }
const DISPLAYED_SRC_CACHE_KEY = 'feishin-displayed-src-cache';
const MAX_DISPLAYED_SRC_CACHE = 500;
function addToDisplayedCache(src: string | undefined) {
if (!src) return;
try {
const cache = getDisplayedSrcCache();
if (cache.includes(src)) {
return;
}
while (cache.length >= MAX_DISPLAYED_SRC_CACHE) {
cache.shift();
}
cache.push(src);
sessionStorage.setItem(DISPLAYED_SRC_CACHE_KEY, JSON.stringify(cache));
} catch {
// ignore error if sessionStorage is unavailable
}
}
function getDisplayedSrcCache(): string[] {
try {
const raw = sessionStorage.getItem(DISPLAYED_SRC_CACHE_KEY);
return raw ? (JSON.parse(raw) as string[]) : [];
} catch {
return [];
}
}
function isInDisplayedCache(src: string | undefined): boolean {
if (!src) return false;
try {
return getDisplayedSrcCache().includes(src);
} catch {
return false;
}
}
export const Image = memo(BaseImage); export const Image = memo(BaseImage);
const ImageContainer = forwardRef( const ImageContainer = forwardRef(