perf(gallery): gate tile decryption until near-viewport
Adds enabled=true param to useDecryptedMediaUrl in MediaGallery.tsx. GalleryTile now uses useNearViewport(300px) — decryption is deferred until the tile approaches the viewport, preventing burst of 100 concurrent decrypt/fetch calls when a pagination batch loads. Blob revocation was already correct (no actual leak); this fixes the load-burst performance issue. Full windowing virtualization deferred. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -119,7 +119,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
|||||||
| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------- | :----- |
|
| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------- | :----- |
|
||||||
| State Sync | Fire-and-forget network call to set offline presence during `pagehide` event may not complete reliably, potentially causing UI drift in presence status. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN |
|
| State Sync | Fire-and-forget network call to set offline presence during `pagehide` event may not complete reliably, potentially causing UI drift in presence status. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN |
|
||||||
| State Sync | Fire-and-forget network call `setPresence().catch(...)` suppresses errors, meaning the app may falsely assume presence update success. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN |
|
| State Sync | Fire-and-forget network call `setPresence().catch(...)` suppresses errors, meaning the app may falsely assume presence update success. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN |
|
||||||
| Memory Leak | Decrypted Media Memory Leak (Gallery & Lightbox) due to missing virtualization and blob revocation. | `cinny/src/app/features/room/MediaGallery.tsx` | OPEN |
|
| Memory Leak | Decrypted Media Memory Leak (Gallery & Lightbox) due to missing virtualization and blob revocation. | `cinny/src/app/features/room/MediaGallery.tsx` | PARTIALLY FIXED ⚠️ UNTESTED — Blob revocation was already correct; added `enabled` param to `useDecryptedMediaUrl` and `useNearViewport(300px)` to each `GalleryTile` to gate decryption until near-viewport, reducing burst on pagination. True virtualization (windowing) deferred — requires significant refactor. |
|
||||||
| Data Persistence | Scheduled Messages are ephemeral (lost on refresh) due to fragile `localStorage` parsing. | `cinny/src/app/state/scheduledMessages.ts` | FIXED — now uses `atomWithStorage` + `createJSONStorage` (Jotai's built-in persistence with error-safe JSON parsing) |
|
| Data Persistence | Scheduled Messages are ephemeral (lost on refresh) due to fragile `localStorage` parsing. | `cinny/src/app/state/scheduledMessages.ts` | FIXED — now uses `atomWithStorage` + `createJSONStorage` (Jotai's built-in persistence with error-safe JSON parsing) |
|
||||||
| Memory Leak | Potential memory leak due to uncleaned `handleMouseMove` listener in `usePan`. | `cinny/src/app/hooks/usePan.ts` | FALSE POSITIVE — `usePan` already uses `attachedRef` to track listeners and cleans them up in an unmount `useEffect`. No change needed. |
|
| Memory Leak | Potential memory leak due to uncleaned `handleMouseMove` listener in `usePan`. | `cinny/src/app/hooks/usePan.ts` | FALSE POSITIVE — `usePan` already uses `attachedRef` to track listeners and cleans them up in an unmount `useEffect`. No change needed. |
|
||||||
| Asset Optimization | Large unoptimized media asset (213KB) found in `public/res`. | `public/res/Lotus.png` | OPEN |
|
| Asset Optimization | Large unoptimized media asset (213KB) found in `public/res`. | `public/res/Lotus.png` | OPEN |
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useNearViewport } from '../../hooks/useNearViewport';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -45,11 +46,13 @@ function useDecryptedMediaUrl(
|
|||||||
encInfo: IEncryptedFile | undefined,
|
encInfo: IEncryptedFile | undefined,
|
||||||
useAuthentication: boolean,
|
useAuthentication: boolean,
|
||||||
mimeType?: string,
|
mimeType?: string,
|
||||||
|
enabled = true,
|
||||||
): DecryptState {
|
): DecryptState {
|
||||||
const [state, setState] = useState<DecryptState>({ status: 'loading' });
|
const [state, setState] = useState<DecryptState>({ status: 'loading' });
|
||||||
const prevBlobUrl = useRef<string | null>(null);
|
const prevBlobUrl = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!enabled) return undefined;
|
||||||
if (!mxcUrl) {
|
if (!mxcUrl) {
|
||||||
setState({ status: 'error' });
|
setState({ status: 'error' });
|
||||||
return;
|
return;
|
||||||
@@ -84,7 +87,7 @@ function useDecryptedMediaUrl(
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [mx, mxcUrl, encInfo, useAuthentication, mimeType]);
|
}, [mx, mxcUrl, encInfo, useAuthentication, mimeType, enabled]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
@@ -367,12 +370,15 @@ function GalleryTile({
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}) {
|
}) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const media = useDecryptedMediaUrl(mx, mxcUrl, encInfo, useAuthentication, mimeType);
|
const tileRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const nearViewport = useNearViewport(tileRef, 300);
|
||||||
|
const media = useDecryptedMediaUrl(mx, mxcUrl, encInfo, useAuthentication, mimeType, nearViewport);
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const relDate = formatRelativeDate(ts);
|
const relDate = formatRelativeDate(ts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
ref={tileRef}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={body || (isVideo ? 'Video' : 'Image')}
|
aria-label={body || (isVideo ? 'Video' : 'Image')}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
Reference in New Issue
Block a user