Skip to content

Commit 8e304f2

Browse files
authored
feat(code): add diff panel virtualization (#1623)
## Problem see thread: https://discord.com/channels/1465397904901673123/1489347523335426269 repos with lots of files or large files in the dirty git state cause performance issues as the diff panel tries to compute and render all of them <!-- Who is this for and what problem does it solve? --> <!-- Closes #ISSUE_ID --> ## Changes adds virtualization to hopefully mitigate the issue <!-- What did you change and why? --> <!-- If there are frontend changes, include screenshots. --> ## How did you test this? manually tested with this state, it was definitely slow, but at least the app did not compeltely die![Screenshot 2026-04-13 at 11.12.44 AM.png](https://app.graphite.com/user-attachments/assets/79feb54c-52c0-4b45-ab4a-936d90dd622e.png)
1 parent f75551e commit 8e304f2

File tree

3 files changed

+72
-33
lines changed

3 files changed

+72
-33
lines changed

apps/code/src/renderer/features/code-review/components/CloudReviewPage.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useMemo } from "react";
1010
import type { DiffOptions } from "../types";
1111
import type { PrCommentThread } from "../utils/prCommentAnnotations";
1212
import { InteractiveFileDiff } from "./InteractiveFileDiff";
13+
import { LazyDiff } from "./LazyDiff";
1314
import {
1415
DeferredDiffPlaceholder,
1516
DiffFileHeader,
@@ -102,15 +103,17 @@ export function CloudReviewPage({ task }: CloudReviewPageProps) {
102103

103104
return (
104105
<div key={file.path} data-file-path={file.path}>
105-
<CloudFileDiff
106-
file={file}
107-
taskId={taskId}
108-
prUrl={prUrl}
109-
options={diffOptions}
110-
collapsed={isCollapsed}
111-
onToggle={() => toggleFile(file.path)}
112-
commentThreads={showReviewComments ? commentThreads : undefined}
113-
/>
106+
<LazyDiff>
107+
<CloudFileDiff
108+
file={file}
109+
taskId={taskId}
110+
prUrl={prUrl}
111+
options={diffOptions}
112+
collapsed={isCollapsed}
113+
onToggle={() => toggleFile(file.path)}
114+
commentThreads={showReviewComments ? commentThreads : undefined}
115+
/>
116+
</LazyDiff>
114117
</div>
115118
);
116119
})}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { type ReactNode, useEffect, useRef, useState } from "react";
2+
3+
const VISIBILITY_MARGIN = 1500;
4+
5+
interface LazyDiffProps {
6+
children: ReactNode;
7+
}
8+
9+
export function LazyDiff({ children }: LazyDiffProps) {
10+
const ref = useRef<HTMLDivElement>(null);
11+
const [mounted, setMounted] = useState(false);
12+
13+
useEffect(() => {
14+
const el = ref.current;
15+
if (!el || mounted) return;
16+
17+
const observer = new IntersectionObserver(
18+
([entry]) => {
19+
if (entry.isIntersecting) {
20+
setMounted(true);
21+
observer.disconnect();
22+
}
23+
},
24+
{ rootMargin: `${VISIBILITY_MARGIN}px 0px` },
25+
);
26+
observer.observe(el);
27+
return () => observer.disconnect();
28+
}, [mounted]);
29+
30+
return <div ref={ref}>{mounted ? children : null}</div>;
31+
}

apps/code/src/renderer/features/code-review/components/ReviewPage.tsx

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useMemo } from "react";
1111
import { useReviewDiffs } from "../hooks/useReviewDiffs";
1212
import type { DiffOptions } from "../types";
1313
import { InteractiveFileDiff } from "./InteractiveFileDiff";
14+
import { LazyDiff } from "./LazyDiff";
1415
import {
1516
DeferredDiffPlaceholder,
1617
type DeferredReason,
@@ -108,14 +109,16 @@ export function ReviewPage({ task }: ReviewPageProps) {
108109
const isCollapsed = collapsedFiles.has(key);
109110
return (
110111
<div key={key} data-file-path={key}>
111-
<UntrackedFileDiff
112-
file={file}
113-
repoPath={repoPath}
114-
options={diffOptions}
115-
collapsed={isCollapsed}
116-
onToggle={() => toggleFile(key)}
117-
taskId={taskId}
118-
/>
112+
<LazyDiff>
113+
<UntrackedFileDiff
114+
file={file}
115+
repoPath={repoPath}
116+
options={diffOptions}
117+
collapsed={isCollapsed}
118+
onToggle={() => toggleFile(key)}
119+
taskId={taskId}
120+
/>
121+
</LazyDiff>
119122
</div>
120123
);
121124
})}
@@ -183,22 +186,24 @@ function FileDiffList({
183186

184187
return (
185188
<div key={key} data-file-path={key}>
186-
<InteractiveFileDiff
187-
fileDiff={fileDiff}
188-
repoPath={repoPath}
189-
options={{ ...diffOptions, collapsed: isCollapsed }}
190-
taskId={taskId}
191-
renderCustomHeader={(fd) => (
192-
<DiffFileHeader
193-
fileDiff={fd}
194-
collapsed={isCollapsed}
195-
onToggle={() => toggleFile(key)}
196-
onOpenFile={() =>
197-
openFile(taskId, `${repoPath}/${filePath}`, false)
198-
}
199-
/>
200-
)}
201-
/>
189+
<LazyDiff>
190+
<InteractiveFileDiff
191+
fileDiff={fileDiff}
192+
repoPath={repoPath}
193+
options={{ ...diffOptions, collapsed: isCollapsed }}
194+
taskId={taskId}
195+
renderCustomHeader={(fd) => (
196+
<DiffFileHeader
197+
fileDiff={fd}
198+
collapsed={isCollapsed}
199+
onToggle={() => toggleFile(key)}
200+
onOpenFile={() =>
201+
openFile(taskId, `${repoPath}/${filePath}`, false)
202+
}
203+
/>
204+
)}
205+
/>
206+
</LazyDiff>
202207
</div>
203208
);
204209
});

0 commit comments

Comments
 (0)