From da8ef12a3802e1758f34778e8445310f56b3e48e Mon Sep 17 00:00:00 2001 From: Manjusaka Date: Thu, 11 Jun 2026 23:44:34 +0800 Subject: [PATCH] fix(detail): stop the switch + zoom-load flicker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two flicker problems reported after the zoom fixes deployed: Switching photos flickered the info panel: - HistogramChart/ToneAnalysis were fed `preview_url`, which on this deployment is the full-resolution (~30MP) image, so every switch RE-FETCHED + RE-DECODED a 30MP image just to compute the histogram (empty → recomputed → popped in). Feed them the same ~1280 display variant the slide already has cached instead (the histogram downscales to ~300px, so a variant is more than enough). No extra fetch, tiny decode. Falls back to preview_url only when there are no variants. - The histogram flashed its backdrop-blur loading spinner (and dimmed its canvas) on every recompute. Show the spinner only on the very first load; on a switch keep the previous histogram visible and let the spring animation morph to the new one. Zoom flickered continuously while the high-res image loaded: - The XHR's `loadingProgress` updates re-render ProgressiveImage many times during a multi-MB load, which re-rendered the WebGL viewer each time. Memoize the viewer so progress ticks don't touch it — its props are stable until the image actually loads. Mount/unmount (the #510 destroy lifecycle) is unchanged. Co-Authored-By: Claude Opus 4.8 --- components/album/histogram-chart.tsx | 10 ++++++++-- components/album/preview-image.tsx | 19 +++++++++++++++++-- components/album/progressive-image.tsx | 12 ++++++++++-- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/components/album/histogram-chart.tsx b/components/album/histogram-chart.tsx index da048a40..4a85c4da 100644 --- a/components/album/histogram-chart.tsx +++ b/components/album/histogram-chart.tsx @@ -374,7 +374,11 @@ export default function HistogramChart({ imageUrl, className = '' }: Readonly - {loading && ( + {/* Only show the loading overlay on the very first computation. On a photo + switch we keep the previous histogram canvas visible and let the spring + animation morph to the new one — flashing this backdrop-blur spinner on + every switch was the panel "flicker". */} + {loading && !histogram && (
@@ -398,7 +402,9 @@ export default function HistogramChart({ imageUrl, className = '' }: Readonly )} diff --git a/components/album/preview-image.tsx b/components/album/preview-image.tsx index ac136b3f..ec4ad726 100644 --- a/components/album/preview-image.tsx +++ b/components/album/preview-image.tsx @@ -299,8 +299,23 @@ export default function PreviewImage(props: Readonly) { return null }, [current?.width, current?.height]) - // Image URL for tone analysis and histogram - const imageUrl = current?.preview_url || current?.url || '' + // Histogram/tone source: the same display-sized variant the current slide has + // already loaded (≈1280, browser-cached → no extra fetch) instead of the raw + // full-resolution `preview_url`. preview compression is off on this deployment, + // so preview_url is the ~30MP original — pointing histogram/tone at it made + // every switch RE-FETCH + RE-DECODE a 30MP image (the panel "flicker"). The + // histogram/tone scale to ~300px, so a 1280 variant is more than enough. Falls + // back to preview_url only when the photo has no generated variants. + const histAvifOk = useAvifSupport() + const histVariantBase = configData?.variantBaseUrl ?? '' + const imageUrl = current && hasReadyVariants(current.image_key, current.ready_max_width, histVariantBase) + ? makeVariantLoader({ + base: histVariantBase, + imageKey: current.image_key, + readyMaxWidth: current.ready_max_width, + format: histAvifOk ? 'avif' : 'webp', + })({ src: current.image_key, width: 1280 }) + : (current?.preview_url || current?.url || '') // Debounce the histogram/tone source: those run an image-load + getImageData + // full-pixel scan, wasteful to fire for every photo flashed past during fast diff --git a/components/album/progressive-image.tsx b/components/album/progressive-image.tsx index 1f555f05..b64b5694 100644 --- a/components/album/progressive-image.tsx +++ b/components/album/progressive-image.tsx @@ -1,12 +1,20 @@ 'use client' import type { ProgressiveImageProps } from '~/types/props.ts' -import { useEffect, useState, useRef, Activity } from 'react' +import { useEffect, useState, useRef, Activity, memo } from 'react' import { createPortal } from 'react-dom' import { useTranslations } from 'next-intl' import { MotionImage } from '~/components/album/motion-image' import { useBlurImageDataUrl } from '~/hooks/use-blurhash' import { WebGLImageViewer } from '~/components/album/webgl-viewer' + +// Memoized so the high-res XHR's loadingProgress ticks (which re-render +// ProgressiveImage many times during a multi-MB load) don't re-render the WebGL +// viewer — its props (src/dimensions) are stable until the image actually loads, +// so without this the viewer re-rendered on every progress tick = the zoom +// "flicker during loading". Only mount/unmount (the #510 destroy lifecycle) and +// a real src change re-touch it. +const MemoWebGLImageViewer = memo(WebGLImageViewer) import type { WebGLImageViewerRef } from '~/components/album/webgl-viewer' import { isWebGLSupported } from '~/lib/utils/webgl' import { hasReadyVariants, makeVariantLoader } from '~/lib/image/loader' @@ -289,7 +297,7 @@ export default function ProgressiveImage( {/* WebGL 图片查看器 */}
-