Skip to content

Commit 6a51b5a

Browse files
authored
🤖 perf: speed up immersive review for large diffs (#3175)
## Summary Immersive Review was doing repeated hot-path work during normal rerenders in large diff-heavy files. This change memoizes review-progress aggregation and active-file comment indicator mapping so keyboard navigation and cursor movement do less work in 3000+ LOC reviews. ## Background A user reported noticeable slowness in Immersive Review for large files with many diffs. The existing render path recalculated changed-line progress across all hunks and rebuilt minimap comment indices from the rendered overlay during routine immersive updates, which made the experience feel heavier than it needed to. ## Implementation - memoize changed-line review progress across `allHunks` - memoize the active file's review list and minimap `commentLineIndices` - reuse stable empty collections to avoid avoidable prop churn into the immersive diff/minimap subtree - destructure `isRead` once so the memo dependencies stay narrow and lint-clean ## Validation - `make static-check` - `bun test src/browser/features/RightSidebar/CodeReview/ImmersiveReviewView.test.tsx` ## Risks Low. The change is scoped to Immersive Review's derived render data and does not alter navigation or review semantics, but stale memo dependencies would show up as incorrect progress or missing comment markers. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$2.23`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=2.23 -->
1 parent 06e1636 commit 6a51b5a

1 file changed

Lines changed: 52 additions & 30 deletions

File tree

src/browser/features/RightSidebar/CodeReview/ImmersiveReviewView.tsx

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ const MAX_HIGHLIGHTED_DIFF_LINES = 4000;
122122
const ACTIVE_LINE_OUTLINE = "1px solid hsl(from var(--color-review-accent) h s l / 0.45)";
123123
const LIKE_NOTE_PREFIX = "I like this change";
124124
const DISLIKE_NOTE_PREFIX = "I don't like this change";
125+
const EMPTY_REVIEWS: Review[] = [];
126+
const EMPTY_COMMENT_LINE_INDICES = new Set<number>();
125127

126128
function getFileBaseName(filePath: string): string {
127129
const segments = filePath.split(/[\\/]/);
@@ -392,25 +394,38 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
392394
onMarkFileAsRead,
393395
onExit,
394396
onReviewNote,
397+
isRead,
395398
isTouchImmersive,
396399
} = props;
397400
const isTouchExperience = isTouchImmersive === true;
398401

399402
// Flatten file tree into ordered file list
400403
const fileList = useMemo(() => flattenFileTreeLeaves(fileTree), [fileTree]);
401-
const reviewedHunkCount = allHunks.filter((item) => props.isRead(item.id)).length;
402-
// Weight immersive progress by changed LoC so a large hunk moves the bar more than a one-line nit.
403-
const totalChangedLineCount = allHunks.reduce(
404-
(count, hunk) => count + getChangedLineCount(hunk),
405-
0
406-
);
407-
const reviewedChangedLineCount = allHunks.reduce((count, hunk) => {
408-
if (!props.isRead(hunk.id)) {
409-
return count;
404+
const reviewProgress = useMemo(() => {
405+
// Cursor movement should stay lightweight even in large diff-heavy files, so memoize
406+
// the per-hunk diff parsing instead of rescanning every hunk on each immersive render.
407+
let reviewedHunkCount = 0;
408+
let totalChangedLineCount = 0;
409+
let reviewedChangedLineCount = 0;
410+
411+
for (const hunk of allHunks) {
412+
const changedLineCount = getChangedLineCount(hunk);
413+
totalChangedLineCount += changedLineCount;
414+
if (isRead(hunk.id)) {
415+
reviewedHunkCount += 1;
416+
reviewedChangedLineCount += changedLineCount;
417+
}
410418
}
411419

412-
return count + getChangedLineCount(hunk);
413-
}, 0);
420+
return {
421+
reviewedHunkCount,
422+
totalChangedLineCount,
423+
reviewedChangedLineCount,
424+
};
425+
}, [allHunks, isRead]);
426+
const reviewedHunkCount = reviewProgress.reviewedHunkCount;
427+
const totalChangedLineCount = reviewProgress.totalChangedLineCount;
428+
const reviewedChangedLineCount = reviewProgress.reviewedChangedLineCount;
414429
const reviewCompletionWidthPercent =
415430
totalChangedLineCount === 0 ? 0 : (reviewedChangedLineCount / totalChangedLineCount) * 100;
416431
const reviewCompletionPercent = Math.round(reviewCompletionWidthPercent);
@@ -646,17 +661,26 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
646661
}),
647662
[props.reviewsByFilePath]
648663
);
664+
const activeFileReviews = useMemo(
665+
() =>
666+
activeFilePath
667+
? (props.reviewsByFilePath.get(activeFilePath) ?? EMPTY_REVIEWS)
668+
: EMPTY_REVIEWS,
669+
[activeFilePath, props.reviewsByFilePath]
670+
);
649671

650-
// Map review line ranges → diff line indices for minimap comment indicators
651-
const commentLineIndices: ReadonlySet<number> = (() => {
652-
if (!activeFilePath || overlayData.content.length === 0) return new Set<number>();
653-
const reviews = props.reviewsByFilePath.get(activeFilePath);
654-
if (!reviews || reviews.length === 0) return new Set<number>();
672+
// Map review line ranges → diff line indices for minimap comment indicators.
673+
// Memoize the line-number lookups so cursor movement does not rebuild multi-thousand-line
674+
// maps when neither the rendered overlay nor the file's review set changed.
675+
const commentLineIndices = useMemo<ReadonlySet<number>>(() => {
676+
if (overlayData.content.length === 0 || activeFileReviews.length === 0) {
677+
return EMPTY_COMMENT_LINE_INDICES;
678+
}
655679

656680
const newLineMap = buildNewLineNumberToIndexMap(overlayData.content);
657681
let oldLineMap: Map<number, number> | null = null;
658682
const indices = new Set<number>();
659-
for (const review of reviews) {
683+
for (const review of activeFileReviews) {
660684
const parsed = parseReviewLineRange(review.data.lineRange);
661685
if (!parsed) continue;
662686

@@ -680,7 +704,7 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
680704
}
681705
}
682706
return indices;
683-
})();
707+
}, [activeFileReviews, overlayData.content]);
684708

685709
const [inlineComposerRequest, setInlineComposerRequest] = useState<InlineComposerRequest | null>(
686710
null
@@ -824,7 +848,7 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
824848
const activeLineIndexRef = useRef<number | null>(null);
825849
const selectedLineRangeRef = useRef<SelectedLineRange | null>(null);
826850
const selectedHunkIdRef = useRef<string | null>(selectedHunkId);
827-
const isReadRef = useRef(props.isRead);
851+
const isReadRef = useRef(isRead);
828852
const onToggleReadRef = useRef(onToggleRead);
829853
const onSelectHunkRef = useRef(onSelectHunk);
830854
const allHunksRef = useRef(allHunks);
@@ -844,8 +868,8 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
844868
}, [selectedHunkId]);
845869

846870
useEffect(() => {
847-
isReadRef.current = props.isRead;
848-
}, [props.isRead]);
871+
isReadRef.current = isRead;
872+
}, [isRead]);
849873

850874
useEffect(() => {
851875
onToggleReadRef.current = onToggleRead;
@@ -1739,12 +1763,12 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
17391763
type="button"
17401764
className={cn(
17411765
"text-muted hover:text-read flex shrink-0 cursor-pointer items-center border-none bg-transparent p-0 transition-colors duration-150 sm:hidden",
1742-
props.isRead(selectedHunk.id) && "text-read"
1766+
isRead(selectedHunk.id) && "text-read"
17431767
)}
17441768
onClick={() => handleToggleReadWithUndo(selectedHunk.id)}
1745-
aria-label={props.isRead(selectedHunk.id) ? "Mark hunk as unread" : "Mark hunk as read"}
1769+
aria-label={isRead(selectedHunk.id) ? "Mark hunk as unread" : "Mark hunk as read"}
17461770
>
1747-
{props.isRead(selectedHunk.id) ? (
1771+
{isRead(selectedHunk.id) ? (
17481772
<Check aria-hidden="true" className="h-3 w-3" />
17491773
) : (
17501774
<Circle aria-hidden="true" className="h-3 w-3" />
@@ -1763,14 +1787,14 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
17631787
type="button"
17641788
className={cn(
17651789
"text-muted hover:text-read flex cursor-pointer items-center border-none bg-transparent p-0 transition-colors duration-150",
1766-
props.isRead(selectedHunk.id) && "text-read"
1790+
isRead(selectedHunk.id) && "text-read"
17671791
)}
17681792
onClick={() => handleToggleReadWithUndo(selectedHunk.id)}
17691793
aria-label={
1770-
props.isRead(selectedHunk.id) ? "Mark hunk as unread" : "Mark hunk as read"
1794+
isRead(selectedHunk.id) ? "Mark hunk as unread" : "Mark hunk as read"
17711795
}
17721796
>
1773-
{props.isRead(selectedHunk.id) ? (
1797+
{isRead(selectedHunk.id) ? (
17741798
<Check aria-hidden="true" className="h-3 w-3" />
17751799
) : (
17761800
<Circle aria-hidden="true" className="h-3 w-3" />
@@ -1890,9 +1914,7 @@ export const ImmersiveReviewView: React.FC<ImmersiveReviewViewProps> = (props) =
18901914
<SelectableDiffRenderer
18911915
content={overlayData.content}
18921916
filePath={activeFilePath ?? currentFileHunks[0].filePath}
1893-
inlineReviews={
1894-
activeFilePath ? props.reviewsByFilePath.get(activeFilePath) : undefined
1895-
}
1917+
inlineReviews={activeFileReviews}
18961918
oldStart={1}
18971919
newStart={1}
18981920
fontSize="11px"

0 commit comments

Comments
 (0)