History

Optimize expanded image dialog rerenders

Cursor Agent committed 5 months ago

Commit c3d55a21 parent b3e8c035 files · +86 −42Browse files

Unified · Split

5 files
  1. apps/web/src/components/chat/ExpandedImageDialog.tsx +72 −44
  2. docs/perf/expanded-image-dialog-react-scan-after.json +8 −0
  3. docs/perf/expanded-image-dialog-react-scan-after.webm +0 −0
  4. docs/perf/expanded-image-dialog-react-scan-before.json +8 −0
  5. docs/perf/expanded-image-dialog-react-scan-before.webm +0 −0
apps/web/src/components/chat/ExpandedImageDialog.tsx+72−44
@@ -1,4 +1,4 @@
1import { memo, useCallback, useEffect, useState } from "react";1import { useEffect, useEffectEvent, useState } from "react";
2import { ChevronLeftIcon, ChevronRightIcon, XIcon } from "lucide-react";2import { ChevronLeftIcon, ChevronRightIcon, XIcon } from "lucide-react";
3import { Button } from "../ui/button";3import { Button } from "../ui/button";
4import type { ExpandedImagePreview } from "./ExpandedImagePreview";4import type { ExpandedImagePreview } from "./ExpandedImagePreview";
@@ -8,52 +8,80 @@ interface ExpandedImageDialogProps { · 3 unmodified lines
8 onClose: () => void;8 onClose: () => void;
9}9}
10 10
11export const ExpandedImageDialog = memo(function ExpandedImageDialog({11export function ExpandedImageDialog({ preview, onClose }: ExpandedImageDialogProps) {
12 preview: initialPreview,12 return (
13 onClose,13 <ExpandedImageDialogContent
14}: ExpandedImageDialogProps) {14 key={getPreviewResetKey(preview)}
15 const [preview, setPreview] = useState(initialPreview);15 preview={preview}
16 16 onClose={onClose}
17 // Sync when the parent hands us a new preview reference.17 />
18 useEffect(() => {18 );
19 setPreview(initialPreview);19}
20 }, [initialPreview]);
21 20
22 const navigateImage = useCallback((direction: -1 | 1) => {21function getPreviewResetKey(preview: ExpandedImagePreview): string {
23 setPreview((existing) => {22 return [
24 if (existing.images.length <= 1) return existing;23 preview.index,
25 const nextIndex =24 preview.images.length,
26 (existing.index + direction + existing.images.length) % existing.images.length;25 ...preview.images.map((image) => `${image.src}\u0000${image.name}`),
27 if (nextIndex === existing.index) return existing;26 ].join("\u0001");
28 return { ...existing, index: nextIndex };27}
29 });
30 }, []);
31 28
32 useEffect(() => {29function useExpandedImageKeyboardShortcuts(input: {
33 const onKeyDown = (event: globalThis.KeyboardEvent) => {30 readonly canNavigate: boolean;
34 if (event.key === "Escape") {31 readonly onClose: () => void;
35 event.preventDefault();32 readonly onNext: () => void;
36 event.stopPropagation();33 readonly onPrevious: () => void;
37 onClose();34}) {
38 return;35 const handleKeyDown = useEffectEvent((event: globalThis.KeyboardEvent) => {
39 }36 if (event.key === "Escape") {
40 if (preview.images.length <= 1) return;37 event.preventDefault();
41 if (event.key === "ArrowLeft") {38 event.stopPropagation();
42 event.preventDefault();39 input.onClose();
43 event.stopPropagation();40 return;
44 navigateImage(-1);41 }
45 return;42 if (!input.canNavigate) return;
46 }43 if (event.key === "ArrowLeft") {
47 if (event.key !== "ArrowRight") return;
48 event.preventDefault();44 event.preventDefault();
49 event.stopPropagation();45 event.stopPropagation();
50 navigateImage(1);46 input.onPrevious();
51 };47 return;
48 }
49 if (event.key !== "ArrowRight") return;
50 event.preventDefault();
51 event.stopPropagation();
52 input.onNext();
53 });
54
55 useEffect(() => {
56 const onKeyDown = (event: globalThis.KeyboardEvent) => handleKeyDown(event);
52 window.addEventListener("keydown", onKeyDown);57 window.addEventListener("keydown", onKeyDown);
53 return () => window.removeEventListener("keydown", onKeyDown);58 return () => window.removeEventListener("keydown", onKeyDown);
54 }, [navigateImage, onClose, preview.images.length]);59 }, []);
60}
55 61
56 const item = preview.images[preview.index];62function ExpandedImageDialogContent({ preview, onClose }: ExpandedImageDialogProps) {
63 const [currentIndex, setCurrentIndex] = useState(preview.index);
64 const images = preview.images;
65 const canNavigate = images.length > 1;
66
67 const navigateImage = (direction: -1 | 1) => {
68 setCurrentIndex((existingIndex) => {
69 if (!canNavigate) return existingIndex;
70 const safeExistingIndex =
71 existingIndex >= 0 && existingIndex < images.length ? existingIndex : preview.index;
72 const nextIndex = (safeExistingIndex + direction + images.length) % images.length;
73 return nextIndex === existingIndex ? existingIndex : nextIndex;
74 });
75 };
76
77 useExpandedImageKeyboardShortcuts({
78 canNavigate,
79 onClose,
80 onNext: () => navigateImage(1),
81 onPrevious: () => navigateImage(-1),
82 });
83
84 const item = images[currentIndex];
57 if (!item) return null;85 if (!item) return null;
58 86
59 return (87 return (
@@ -69,7 +97,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 9 unmodified lines
69 aria-label="Close image preview"97 aria-label="Close image preview"
70 onClick={onClose}98 onClick={onClose}
71 />99 />
72 {preview.images.length > 1 && (100 {canNavigate && (
73 <Button101 <Button
74 type="button"102 type="button"
75 size="icon"103 size="icon"
@@ -100,10 +128,10 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 24 unmodified lines
100 />128 />
101 <p className="mt-2 max-w-[92vw] truncate text-center text-xs text-muted-foreground/80">129 <p className="mt-2 max-w-[92vw] truncate text-center text-xs text-muted-foreground/80">
102 {item.name}130 {item.name}
103 {preview.images.length > 1 ? ` (${preview.index + 1}/${preview.images.length})` : ""}131 {canNavigate ? ` (${currentIndex + 1}/${images.length})` : ""}
104 </p>132 </p>
105 </div>133 </div>
106 {preview.images.length > 1 && (134 {canNavigate && (
107 <Button135 <Button
108 type="button"136 type="button"
109 size="icon"137 size="icon"
@@ -117,4 +145,4 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 7 unmodified lines
117 )}145 )}
118 </div>146 </div>
119 );147 );
120});148}
docs/perf/expanded-image-dialog-react-scan-after.json+8−0
@@ -0,0 +1,8 @@
1{
2 "hasReactScan": true,
3 "summary": "Profiler commits: 3 total (1 mount, 2 update), last 1.30ms",
4 "report": {
5 "reactScanReport": null,
6 "renderEvents": []
7 }
8}
docs/perf/expanded-image-dialog-react-scan-after.webm+0−0

Binary file not shown.

docs/perf/expanded-image-dialog-react-scan-before.json+8−0
@@ -0,0 +1,8 @@
1{
2 "hasReactScan": true,
3 "summary": "Profiler commits: 5 total (1 mount, 4 update), last 0.20ms",
4 "report": {
5 "reactScanReport": null,
6 "renderEvents": []
7 }
8}
docs/perf/expanded-image-dialog-react-scan-before.webm+0−0

Binary file not shown.