perf(preview): debounce full-res /proxy preload by 400ms

Rapid arrow-nav was firing one /proxy fetch per photo with no way to
abort (new Image() has no abort). Holding the right arrow through ten
photos in two seconds left ten multi-MB transfers in flight competing
for bandwidth and the RAW/HEIC transcoder. Now the preload only kicks
in if the user lingers on a photo for 400ms; otherwise the timer is
cleared and no /proxy request is made.
This commit is contained in:
Claudio
2026-05-11 11:06:36 +02:00
parent 11202a92e7
commit 9e9b1ba224

View File

@@ -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])