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
147import { PullRequestThreadDialog } from "./PullRequestThreadDialog";147import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
148import { MessagesTimeline } from "./chat/MessagesTimeline";148import { MessagesTimeline } from "./chat/MessagesTimeline";
149import { ChatHeader } from "./chat/ChatHeader";149import { ChatHeader } from "./chat/ChatHeader";
150import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview";150import { getExpandedImagePreviewKey, type ExpandedImagePreview } from "./chat/ExpandedImagePreview";
151import { NoActiveThreadState } from "./NoActiveThreadState";151import { NoActiveThreadState } from "./NoActiveThreadState";
152import { resolveEffectiveEnvMode, resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";152import { resolveEffectiveEnvMode, resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";
153import { ProviderStatusBanner } from "./chat/ProviderStatusBanner";153import { ProviderStatusBanner } from "./chat/ProviderStatusBanner";
@@ -3773,7 +3773,11 @@ export default function ChatView(props: ChatViewProps) { · 3619 unmodified lines
3773 ) : null}3773 ) : null}
3774 3774
3775 {expandedImage && (3775 {expandedImage && (
3776 <ExpandedImageDialog preview={expandedImage} onClose={closeExpandedImage} />3776 <ExpandedImageDialog
3777 key={getExpandedImagePreviewKey(expandedImage)}
3778 preview={expandedImage}
3779 onClose={closeExpandedImage}
3780 />
3777 )}3781 )}
3778 </div>3782 </div>
3779 );3783 );
apps/web/src/components/chat/ExpandedImageDialog.tsx+52−28
@@ -8,27 +8,20 @@ interface ExpandedImageDialogProps { · 7 unmodified lines
8 onClose: () => void;8 onClose: () => void;
9}9}
10 10
11export const ExpandedImageDialog = memo(function ExpandedImageDialog({11function clampImageIndex(index: number, imageCount: number): number {
12 preview: initialPreview,12 if (imageCount <= 0) return 0;
13 onClose,13 return Math.min(Math.max(index, 0), imageCount - 1);
14}: ExpandedImageDialogProps) {14}
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 }, []);
31 15
16function useExpandedImageDialogKeyboardNavigation({
17 imageCount,
18 navigateImage,
19 onClose,
20}: {
21 imageCount: number;
22 navigateImage: (direction: -1 | 1) => void;
23 onClose: () => void;
24}) {
32 useEffect(() => {25 useEffect(() => {
33 const onKeyDown = (event: globalThis.KeyboardEvent) => {26 const onKeyDown = (event: globalThis.KeyboardEvent) => {
34 if (event.key === "Escape") {27 if (event.key === "Escape") {
@@ -37,7 +30,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 2 unmodified lines
37 onClose();30 onClose();
38 return;31 return;
39 }32 }
40 if (preview.images.length <= 1) return;33 if (imageCount <= 1) return;
41 if (event.key === "ArrowLeft") {34 if (event.key === "ArrowLeft") {
42 event.preventDefault();35 event.preventDefault();
43 event.stopPropagation();36 event.stopPropagation();
@@ -51,9 +44,40 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 7 unmodified lines
51 };44 };
52 window.addEventListener("keydown", onKeyDown);45 window.addEventListener("keydown", onKeyDown);
53 return () => window.removeEventListener("keydown", onKeyDown);46 return () => window.removeEventListener("keydown", onKeyDown);
54 }, [navigateImage, onClose, preview.images.length]);47 }, [imageCount, navigateImage, onClose]);
48}
49
50export 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 });
55 79
56 const item = preview.images[preview.index];80 const item = preview.images[activeImageIndex];
57 if (!item) return null;81 if (!item) return null;
58 82
59 return (83 return (
@@ -69,14 +93,14 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 9 unmodified lines
69 aria-label="Close image preview"93 aria-label="Close image preview"
70 onClick={onClose}94 onClick={onClose}
71 />95 />
72 {preview.images.length > 1 && (96 {imageCount > 1 && (
73 <Button97 <Button
74 type="button"98 type="button"
75 size="icon"99 size="icon"
76 variant="ghost"100 variant="ghost"
77 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"101 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"
78 aria-label="Previous image"102 aria-label="Previous image"
79 onClick={() => navigateImage(-1)}103 onClick={navigateToPreviousImage}
80 >104 >
81 <ChevronLeftIcon className="size-5" />105 <ChevronLeftIcon className="size-5" />
82 </Button>106 </Button>
@@ -100,17 +124,17 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 17 unmodified lines
100 />124 />
101 <p className="mt-2 max-w-[92vw] truncate text-center text-xs text-muted-foreground/80">125 <p className="mt-2 max-w-[92vw] truncate text-center text-xs text-muted-foreground/80">
102 {item.name}126 {item.name}
103 {preview.images.length > 1 ? ` (${preview.index + 1}/${preview.images.length})` : ""}127 {imageCount > 1 ? ` (${activeImageIndex + 1}/${imageCount})` : ""}
104 </p>128 </p>
105 </div>129 </div>
106 {preview.images.length > 1 && (130 {imageCount > 1 && (
107 <Button131 <Button
108 type="button"132 type="button"
109 size="icon"133 size="icon"
110 variant="ghost"134 variant="ghost"
111 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"135 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"
112 aria-label="Next image"136 aria-label="Next image"
113 onClick={() => navigateImage(1)}137 onClick={navigateToNextImage}
114 >138 >
115 <ChevronRightIcon className="size-5" />139 <ChevronRightIcon className="size-5" />
116 </Button>140 </Button>
apps/web/src/components/chat/ExpandedImagePreview.tsx+5−0
@@ -8,6 +8,11 @@ export interface ExpandedImagePreview { · 7 unmodified lines
8 index: number;8 index: number;
9}9}
10 10
11export function getExpandedImagePreviewKey(preview: ExpandedImagePreview): string {
12 const selectedImage = preview.images[preview.index];
13 return `${preview.index}:${preview.images.length}:${selectedImage?.src ?? ""}`;
14}
15
11export function buildExpandedImagePreview(16export function buildExpandedImagePreview(
12 images: ReadonlyArray<{ id: string; name: string; previewUrl?: string }>,17 images: ReadonlyArray<{ id: string; name: string; previewUrl?: string }>,
13 selectedImageId: string,18 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.