fix(gallery): release the canvas decode when it is drawn, not at unmount
Review follow-up. The release only ran from the effect cleanup, so it fired on unmount or a src change — while the commit message and the test header both explained that grid tiles never unmount, which is the whole reason the decode piles up. For the case the change exists for, it never ran at all. drawToCanvas now reports whether it drew, and the source Image is released as soon as the pixels are on the canvas. Nothing redraws from imageRef afterwards; drawToCanvas has exactly one caller. The cleanup stays as the fallback for the paths onload cannot cover: the draw failed, or the source changed before onload fired. The new test pins release while still mounted, on the same src. It fails against the previous version. Refs #1287
This commit is contained in:
@@ -98,14 +98,16 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
|
||||
// Draw image to canvas when canvas rendering is enabled
|
||||
// Returns whether the pixels actually made it onto the canvas, so the
|
||||
// caller knows if the source image is still needed (#1287).
|
||||
const drawToCanvas = useCallback(() => {
|
||||
if (!useCanvasRendering || !canvasRef.current || !imageRef.current) return;
|
||||
if (!useCanvasRendering || !canvasRef.current || !imageRef.current) return false;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const img = imageRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx || !img.complete || img.naturalWidth === 0) return;
|
||||
if (!ctx || !img.complete || img.naturalWidth === 0) return false;
|
||||
|
||||
// Set canvas dimensions to match image
|
||||
canvas.width = img.naturalWidth;
|
||||
@@ -115,6 +117,7 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
setCanvasReady(true);
|
||||
return true;
|
||||
}, [useCanvasRendering]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -271,7 +274,18 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
|
||||
img.onload = () => {
|
||||
imageRef.current = img;
|
||||
drawToCanvas();
|
||||
const drawn = drawToCanvas();
|
||||
// Once drawImage has copied the pixels into the canvas the source
|
||||
// decode is dead weight, so drop it here rather than at unmount. The
|
||||
// grid is not virtualised — a 546-photo event mounts 546 of these and
|
||||
// none of them unmount while the gallery is open — so a cleanup-only
|
||||
// release never actually runs for the case it was meant to fix
|
||||
// (#1287). Nothing redraws from `imageRef` afterwards: drawToCanvas
|
||||
// has this one caller.
|
||||
if (drawn) {
|
||||
imageRef.current = null;
|
||||
img.removeAttribute('src');
|
||||
}
|
||||
onLoad?.();
|
||||
};
|
||||
|
||||
@@ -286,16 +300,13 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
return () => {
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
// Release the decoded bitmap (#1287). `imageRef` is what drawToCanvas
|
||||
// reads, and it was never cleared — so a detached Image, and the
|
||||
// decode behind it, stayed pinned by a live JS reference for the
|
||||
// lifetime of the component. A decoded <img> in the document is
|
||||
// evictable under memory pressure; one held by a ref is not.
|
||||
//
|
||||
// This matters at gallery scale because the grid is not virtualised:
|
||||
// a 546-photo event mounts 546 of these and none of them ever unmount,
|
||||
// so nothing was ever released. Dropping the src first lets the
|
||||
// browser reclaim the decode without waiting for GC to notice.
|
||||
// Fallback release for the paths the onload handler above cannot
|
||||
// cover: the draw failed, or the source changed / the component
|
||||
// unmounted before onload ever fired. `imageRef` is what drawToCanvas
|
||||
// reads and it was never cleared, so a detached Image — and the decode
|
||||
// behind it — stayed pinned by a live JS reference. A decoded <img> in
|
||||
// the document is evictable under memory pressure; one held by a ref
|
||||
// is not.
|
||||
if (imageRef.current === img) {
|
||||
imageRef.current = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user