History

Optimize expanded image dialog rerenders

Cursor Agent committed 6 months ago

Commit d9380cd1 parent d1e85c45 files · +61 −28Browse files

Unified · Split

5 files
  1. apps/web/src/components/ChatView.tsx +6 −2
  2. apps/web/src/components/chat/ExpandedImageDialog.tsx +52 −28
  3. apps/web/src/components/chat/ExpandedImagePreview.tsx +5 −0
  4. docs/performance/expanded-image-dialog-react-scan-after.webm +0 −0
  5. docs/performance/expanded-image-dialog-react-scan-before.webm +0 −0
apps/web/src/components/ChatView.tsx+6−2
@@ -147,7 +147,7 @@ import { ExpandedImageDialog } from "./chat/ExpandedImageDialog"; · 146 unmodified lines
147147 import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
148148 import { MessagesTimeline } from "./chat/MessagesTimeline";
149149 import { ChatHeader } from "./chat/ChatHeader";
150−import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview";
150+import { getExpandedImagePreviewKey, type ExpandedImagePreview } from "./chat/ExpandedImagePreview";
151151 import { NoActiveThreadState } from "./NoActiveThreadState";
152152 import { resolveEffectiveEnvMode, resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";
153153 import { ProviderStatusBanner } from "./chat/ProviderStatusBanner";
@@ -3773,7 +3773,11 @@ export default function ChatView(props: ChatViewProps) { · 3619 unmodified lines
37733773 ) : null}
37743774
37753775 {expandedImage && (
3776− <ExpandedImageDialog preview={expandedImage} onClose={closeExpandedImage} />
3776+ <ExpandedImageDialog
3777+ key={getExpandedImagePreviewKey(expandedImage)}
3778+ preview={expandedImage}
3779+ onClose={closeExpandedImage}
3780+ />
37773781 )}
37783782 </div>
37793783 );
apps/web/src/components/chat/ExpandedImageDialog.tsx+52−28
@@ -8,27 +8,20 @@ interface ExpandedImageDialogProps { · 7 unmodified lines
88 onClose: () => void;
99 }
1010
11−export const ExpandedImageDialog = memo(function ExpandedImageDialog({
12− preview: initialPreview,
13− onClose,
14−}: ExpandedImageDialogProps) {
15− const [preview, setPreview] = useState(initialPreview);
16−
17− // Sync when the parent hands us a new preview reference.
18− useEffect(() => {
19− setPreview(initialPreview);
20− }, [initialPreview]);
21−
22− const navigateImage = useCallback((direction: -1 | 1) => {
23− setPreview((existing) => {
24− if (existing.images.length <= 1) return existing;
25− const nextIndex =
26− (existing.index + direction + existing.images.length) % existing.images.length;
27− if (nextIndex === existing.index) return existing;
28− return { ...existing, index: nextIndex };
29− });
30− }, []);
11+function clampImageIndex(index: number, imageCount: number): number {
12+ if (imageCount <= 0) return 0;
13+ return Math.min(Math.max(index, 0), imageCount - 1);
14+}
3115
16+function useExpandedImageDialogKeyboardNavigation({
17+ imageCount,
18+ navigateImage,
19+ onClose,
20+}: {
21+ imageCount: number;
22+ navigateImage: (direction: -1 | 1) => void;
23+ onClose: () => void;
24+}) {
3225 useEffect(() => {
3326 const onKeyDown = (event: globalThis.KeyboardEvent) => {
3427 if (event.key === "Escape") {
@@ -37,7 +30,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 2 unmodified lines
3730 onClose();
3831 return;
3932 }
40− if (preview.images.length <= 1) return;
33+ if (imageCount <= 1) return;
4134 if (event.key === "ArrowLeft") {
4235 event.preventDefault();
4336 event.stopPropagation();
@@ -51,9 +44,40 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 7 unmodified lines
5144 };
5245 window.addEventListener("keydown", onKeyDown);
5346 return () => window.removeEventListener("keydown", onKeyDown);
54− }, [navigateImage, onClose, preview.images.length]);
47+ }, [imageCount, navigateImage, onClose]);
48+}
49+
50+export const ExpandedImageDialog = memo(function ExpandedImageDialog({
51+ preview,
52+ onClose,
53+}: ExpandedImageDialogProps) {
54+ const imageCount = preview.images.length;
55+ const [imageIndex, setImageIndex] = useState(() => clampImageIndex(preview.index, imageCount));
56+ const activeImageIndex = clampImageIndex(imageIndex, imageCount);
57+
58+ const navigateImage = useCallback(
59+ (direction: -1 | 1) => {
60+ setImageIndex((existing) => {
61+ if (imageCount <= 1) return existing;
62+ return (existing + direction + imageCount) % imageCount;
63+ });
64+ },
65+ [imageCount],
66+ );
67+ const navigateToPreviousImage = useCallback(() => {
68+ navigateImage(-1);
69+ }, [navigateImage]);
70+ const navigateToNextImage = useCallback(() => {
71+ navigateImage(1);
72+ }, [navigateImage]);
73+
74+ useExpandedImageDialogKeyboardNavigation({
75+ imageCount,
76+ navigateImage,
77+ onClose,
78+ });
5579
56− const item = preview.images[preview.index];
80+ const item = preview.images[activeImageIndex];
5781 if (!item) return null;
5882
5983 return (
@@ -69,14 +93,14 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 9 unmodified lines
6993 aria-label="Close image preview"
7094 onClick={onClose}
7195 />
72− {preview.images.length > 1 && (
96+ {imageCount > 1 && (
7397 <Button
7498 type="button"
7599 size="icon"
76100 variant="ghost"
77101 className="absolute left-2 top-1/2 z-20 -translate-y-1/2 text-white/90 hover:bg-white/10 hover:text-white sm:left-6"
78102 aria-label="Previous image"
79− onClick={() => navigateImage(-1)}
103+ onClick={navigateToPreviousImage}
80104 >
81105 <ChevronLeftIcon className="size-5" />
82106 </Button>
@@ -100,17 +124,17 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 17 unmodified lines
100124 />
101125 <p className="mt-2 max-w-[92vw] truncate text-center text-xs text-muted-foreground/80">
102126 {item.name}
103− {preview.images.length > 1 ? ` (${preview.index + 1}/${preview.images.length})` : ""}
127+ {imageCount > 1 ? ` (${activeImageIndex + 1}/${imageCount})` : ""}
104128 </p>
105129 </div>
106− {preview.images.length > 1 && (
130+ {imageCount > 1 && (
107131 <Button
108132 type="button"
109133 size="icon"
110134 variant="ghost"
111135 className="absolute right-2 top-1/2 z-20 -translate-y-1/2 text-white/90 hover:bg-white/10 hover:text-white sm:right-6"
112136 aria-label="Next image"
113− onClick={() => navigateImage(1)}
137+ onClick={navigateToNextImage}
114138 >
115139 <ChevronRightIcon className="size-5" />
116140 </Button>
apps/web/src/components/chat/ExpandedImagePreview.tsx+5−0
@@ -8,6 +8,11 @@ export interface ExpandedImagePreview { · 7 unmodified lines
88 index: number;
99 }
1010
11+export function getExpandedImagePreviewKey(preview: ExpandedImagePreview): string {
12+ const selectedImage = preview.images[preview.index];
13+ return `${preview.index}:${preview.images.length}:${selectedImage?.src ?? ""}`;
14+}
15+
1116 export function buildExpandedImagePreview(
1217 images: ReadonlyArray<{ id: string; name: string; previewUrl?: string }>,
1318 selectedImageId: string,
docs/performance/expanded-image-dialog-react-scan-after.webm+0−0

Binary file not shown.

docs/performance/expanded-image-dialog-react-scan-before.webm+0−0

Binary file not shown.