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 图片查看器 */}
-