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 @@
1−import { memo, useCallback, useEffect, useState } from "react";
1+import { useEffect, useEffectEvent, useState } from "react";
22 import { ChevronLeftIcon, ChevronRightIcon, XIcon } from "lucide-react";
33 import { Button } from "../ui/button";
44 import type { ExpandedImagePreview } from "./ExpandedImagePreview";
@@ -8,52 +8,80 @@ interface ExpandedImageDialogProps { · 3 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]);
11+export function ExpandedImageDialog({ preview, onClose }: ExpandedImageDialogProps) {
12+ return (
13+ <ExpandedImageDialogContent
14+ key={getPreviewResetKey(preview)}
15+ preview={preview}
16+ onClose={onClose}
17+ />
18+ );
19+}
2120
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− }, []);
21+function getPreviewResetKey(preview: ExpandedImagePreview): string {
22+ return [
23+ preview.index,
24+ preview.images.length,
25+ ...preview.images.map((image) => `${image.src}\u0000${image.name}`),
26+ ].join("\u0001");
27+}
3128
32− useEffect(() => {
33− const onKeyDown = (event: globalThis.KeyboardEvent) => {
34− if (event.key === "Escape") {
35− event.preventDefault();
36− event.stopPropagation();
37− onClose();
38− return;
39− }
40− if (preview.images.length <= 1) return;
41− if (event.key === "ArrowLeft") {
42− event.preventDefault();
43− event.stopPropagation();
44− navigateImage(-1);
45− return;
46− }
47− if (event.key !== "ArrowRight") return;
29+function useExpandedImageKeyboardShortcuts(input: {
30+ readonly canNavigate: boolean;
31+ readonly onClose: () => void;
32+ readonly onNext: () => void;
33+ readonly onPrevious: () => void;
34+}) {
35+ const handleKeyDown = useEffectEvent((event: globalThis.KeyboardEvent) => {
36+ if (event.key === "Escape") {
37+ event.preventDefault();
38+ event.stopPropagation();
39+ input.onClose();
40+ return;
41+ }
42+ if (!input.canNavigate) return;
43+ if (event.key === "ArrowLeft") {
4844 event.preventDefault();
4945 event.stopPropagation();
50− navigateImage(1);
51− };
46+ input.onPrevious();
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);
5257 window.addEventListener("keydown", onKeyDown);
5358 return () => window.removeEventListener("keydown", onKeyDown);
54− }, [navigateImage, onClose, preview.images.length]);
59+ }, []);
60+}
5561
56− const item = preview.images[preview.index];
62+function 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];
5785 if (!item) return null;
5886
5987 return (
@@ -69,7 +97,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 9 unmodified lines
6997 aria-label="Close image preview"
7098 onClick={onClose}
7199 />
72− {preview.images.length > 1 && (
100+ {canNavigate && (
73101 <Button
74102 type="button"
75103 size="icon"
@@ -100,10 +128,10 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 24 unmodified lines
100128 />
101129 <p className="mt-2 max-w-[92vw] truncate text-center text-xs text-muted-foreground/80">
102130 {item.name}
103− {preview.images.length > 1 ? ` (${preview.index + 1}/${preview.images.length})` : ""}
131+ {canNavigate ? ` (${currentIndex + 1}/${images.length})` : ""}
104132 </p>
105133 </div>
106− {preview.images.length > 1 && (
134+ {canNavigate && (
107135 <Button
108136 type="button"
109137 size="icon"
@@ -117,4 +145,4 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({ · 7 unmodified lines
117145 )}
118146 </div>
119147 );
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.