Skip to content

Commit 91e803d

Browse files
fix: Improve symbol reference/definition list perf (#327)
1 parent 81a9ea1 commit 91e803d

File tree

3 files changed

+118
-45
lines changed

3 files changed

+118
-45
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111
- Fixed issue with the symbol hover popover clipping at the top of the page. [#326](https://github.com/sourcebot-dev/sourcebot/pull/326)
12+
- Fixed slow rendering issue with large reference/definition lists. [#327](https://github.com/sourcebot-dev/sourcebot/pull/327)
1213

1314
## [4.1.0] - 2025-06-02
1415

packages/web/src/app/[domain]/components/lightweightCodeHighlighter.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ interface LightweightCodeHighlighter {
2727
renderWhitespace?: boolean;
2828
}
2929

30+
// The maximum number of characters per line that we will display in the preview.
31+
const MAX_NUMBER_OF_CHARACTER_PER_LINE = 1000;
32+
3033
/**
3134
* Lightweight code highlighter that uses the Lezer parser to highlight code.
3235
* This is helpful in scenarios where we need to highlight a ton of code snippets
@@ -49,12 +52,19 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
4952
return code.trimEnd().split('\n');
5053
}, [code]);
5154

55+
const isFileTooLargeToDisplay = useMemo(() => {
56+
return unhighlightedLines.some(line => line.length > MAX_NUMBER_OF_CHARACTER_PER_LINE);
57+
}, [code]);
5258

5359
const [highlightedLines, setHighlightedLines] = useState<React.ReactNode[] | null>(null);
5460

5561
const highlightStyle = useCodeMirrorHighlighter();
5662

5763
useEffect(() => {
64+
if (isFileTooLargeToDisplay) {
65+
return;
66+
}
67+
5868
measure(() => Promise.all(
5969
unhighlightedLines
6070
.map(async (line, index) => {
@@ -103,12 +113,21 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
103113
const lineNumberDigits = String(lineCount).length;
104114
const lineNumberWidth = `${lineNumberDigits + 2}ch`; // +2 for padding
105115

116+
if (isFileTooLargeToDisplay) {
117+
return (
118+
<div className="font-mono text-sm px-2">
119+
File too large to display in preview.
120+
</div>
121+
);
122+
}
123+
106124
return (
107125
<div
108126
style={{
109127
fontFamily: tailwind.theme.fontFamily.editor,
110128
fontSize: tailwind.theme.fontSize.editor,
111129
whiteSpace: renderWhitespace ? 'pre-wrap' : 'none',
130+
wordBreak: 'break-all',
112131
}}
113132
>
114133
{(highlightedLines ?? unhighlightedLines).map((line, index) => (

packages/web/src/ee/features/codeNav/components/exploreMenu/referenceList.tsx

Lines changed: 98 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
44
import { FileHeader } from "@/app/[domain]/components/fileHeader";
55
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
6-
import { ScrollArea } from "@/components/ui/scroll-area";
76
import { FindRelatedSymbolsResponse } from "@/features/codeNav/types";
87
import { RepositoryInfo, SourceRange } from "@/features/search/types";
98
import { base64Decode } from "@/lib/utils";
10-
import { useMemo } from "react";
9+
import { useMemo, useRef } from "react";
1110
import useCaptureEvent from "@/hooks/useCaptureEvent";
11+
import { useVirtualizer } from "@tanstack/react-virtual";
1212

1313
interface ReferenceListProps {
1414
data: FindRelatedSymbolsResponse;
1515
revisionName: string;
1616
}
1717

18+
const ESTIMATED_LINE_HEIGHT_PX = 30;
19+
const ESTIMATED_MATCH_CONTAINER_HEIGHT_PX = 30;
20+
1821
export const ReferenceList = ({
1922
data,
2023
revisionName,
@@ -29,52 +32,102 @@ export const ReferenceList = ({
2932
const { navigateToPath } = useBrowseNavigation();
3033
const captureEvent = useCaptureEvent();
3134

32-
return (
33-
<ScrollArea className="h-full">
34-
{data.files.map((file, index) => {
35-
const repoInfo = repoInfoMap[file.repositoryId];
35+
// Virtualization setup
36+
const parentRef = useRef<HTMLDivElement>(null);
37+
const virtualizer = useVirtualizer({
38+
count: data.files.length,
39+
getScrollElement: () => parentRef.current,
40+
estimateSize: (index) => {
41+
const file = data.files[index];
42+
43+
const estimatedSize =
44+
file.matches.length * ESTIMATED_LINE_HEIGHT_PX +
45+
ESTIMATED_MATCH_CONTAINER_HEIGHT_PX;
46+
47+
return estimatedSize;
48+
},
49+
overscan: 5,
50+
enabled: true,
51+
});
3652

37-
return (
38-
<div key={index}>
39-
<div className="bg-accent py-1 px-2 flex flex-row sticky top-0">
40-
<FileHeader
41-
repo={{
42-
name: repoInfo.name,
43-
displayName: repoInfo.displayName,
44-
codeHostType: repoInfo.codeHostType,
45-
webUrl: repoInfo.webUrl,
53+
return (
54+
<div
55+
ref={parentRef}
56+
style={{
57+
width: "100%",
58+
height: "100%",
59+
overflowY: "auto",
60+
contain: "strict",
61+
}}
62+
>
63+
<div
64+
style={{
65+
height: virtualizer.getTotalSize(),
66+
width: "100%",
67+
position: "relative",
68+
}}
69+
>
70+
{virtualizer.getVirtualItems().map((virtualRow) => {
71+
const file = data.files[virtualRow.index];
72+
const repoInfo = repoInfoMap[file.repositoryId];
73+
return (
74+
<div
75+
key={virtualRow.key}
76+
data-index={virtualRow.index}
77+
ref={virtualizer.measureElement}
78+
style={{
79+
position: "absolute",
80+
transform: `translateY(${virtualRow.start}px)`,
81+
top: 0,
82+
left: 0,
83+
width: "100%",
84+
}}
85+
>
86+
<div
87+
className="bg-accent py-1 px-2 flex flex-row sticky top-0 z-10"
88+
style={{
89+
top: `-${virtualRow.start}px`,
4690
}}
47-
fileName={file.fileName}
48-
branchDisplayName={revisionName === "HEAD" ? undefined : revisionName}
49-
/>
50-
</div>
51-
<div className="divide-y">
52-
{file.matches
53-
.sort((a, b) => a.range.start.lineNumber - b.range.start.lineNumber)
54-
.map((match, index) => (
55-
<ReferenceListItem
56-
key={index}
57-
lineContent={match.lineContent}
58-
range={match.range}
59-
language={file.language}
60-
onClick={() => {
61-
captureEvent('wa_explore_menu_reference_clicked', {});
62-
navigateToPath({
63-
repoName: file.repository,
64-
revisionName,
65-
path: file.fileName,
66-
pathType: 'blob',
67-
highlightRange: match.range,
68-
})
69-
}}
70-
/>
71-
))}
91+
>
92+
<FileHeader
93+
repo={{
94+
name: repoInfo.name,
95+
displayName: repoInfo.displayName,
96+
codeHostType: repoInfo.codeHostType,
97+
webUrl: repoInfo.webUrl,
98+
}}
99+
fileName={file.fileName}
100+
branchDisplayName={revisionName === "HEAD" ? undefined : revisionName}
101+
/>
102+
</div>
103+
<div className="divide-y">
104+
{file.matches
105+
.sort((a, b) => a.range.start.lineNumber - b.range.start.lineNumber)
106+
.map((match, index) => (
107+
<ReferenceListItem
108+
key={index}
109+
lineContent={match.lineContent}
110+
range={match.range}
111+
language={file.language}
112+
onClick={() => {
113+
captureEvent('wa_explore_menu_reference_clicked', {});
114+
navigateToPath({
115+
repoName: file.repository,
116+
revisionName,
117+
path: file.fileName,
118+
pathType: 'blob',
119+
highlightRange: match.range,
120+
})
121+
}}
122+
/>
123+
))}
124+
</div>
72125
</div>
73-
</div>
74-
)
75-
})}
76-
</ScrollArea>
77-
)
126+
);
127+
})}
128+
</div>
129+
</div>
130+
);
78131
}
79132

80133

0 commit comments

Comments
 (0)