diff --git a/frontend/src/components/preview/PreviewImage.tsx b/frontend/src/components/preview/PreviewImage.tsx index 09d070b..6c65232 100644 --- a/frontend/src/components/preview/PreviewImage.tsx +++ b/frontend/src/components/preview/PreviewImage.tsx @@ -59,21 +59,30 @@ function PreviewStillImage({ photo }: { photo: Photo }) { const fullResSrc = getPreviewFullResSrc(photo) const src = usingFullRes ? fullResSrc : thumbSrc - // Reset everything when the photo changes, then start the background - // full-res preload. Cancel the preloader's callback on unmount/change so a - // late-arriving onload from the previous photo can't flip state for the - // current one. + // Reset everything when the photo changes, then queue a *debounced* + // background full-res preload. Debouncing matters because `new Image()` + // requests can't be aborted: rapid arrow-nav would otherwise leave a + // dozen multi-MB /proxy fetches in flight, saturating the user's + // bandwidth (and the backend's transcoder for RAW/HEIC) for photos the + // user already navigated past. Only photos the user lingers on for + // FULL_RES_PRELOAD_DELAY_MS trigger the /proxy fetch. useEffect(() => { setLoaded(false) setUsingFullRes(false) setScale(1) setOffset({ x: 0, y: 0 }) - const preloader = new Image() - preloader.onload = () => setUsingFullRes(true) - preloader.src = fullResSrc + const FULL_RES_PRELOAD_DELAY_MS = 400 + let preloader: HTMLImageElement | null = null + const timer = window.setTimeout(() => { + preloader = new Image() + preloader.onload = () => setUsingFullRes(true) + preloader.src = fullResSrc + }, FULL_RES_PRELOAD_DELAY_MS) + return () => { - preloader.onload = null + window.clearTimeout(timer) + if (preloader) preloader.onload = null } }, [photo.id, fullResSrc])