Skip to content

Commit 2402c6f

Browse files
committed
fix: performance issues with code renderer (#911)
1 parent 317e97e commit 2402c6f

File tree

6 files changed

+106
-46
lines changed

6 files changed

+106
-46
lines changed

docker-compose.dev.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
postgres:
3-
image: postgres:15
3+
image: postgres:16
44
restart: unless-stopped
55
environment:
66
- POSTGRES_USER=postgres

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"react-dom": "^19.1.1",
7979
"react-markdown": "^10.1.0",
8080
"react-router-dom": "^7.8.2",
81+
"react-window": "1.8.11",
8182
"remark-gfm": "^4.0.1",
8283
"sharp": "^0.34.3",
8384
"swr": "^2.3.6",
@@ -96,6 +97,7 @@
9697
"@types/qrcode": "^1.5.5",
9798
"@types/react": "^19.1.12",
9899
"@types/react-dom": "^19.1.9",
100+
"@types/react-window": "^1.8.8",
99101
"@vitejs/plugin-react": "^5.0.2",
100102
"eslint": "^9.34.0",
101103
"eslint-config-prettier": "^10.1.8",

pnpm-lock.yaml

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/file/DashboardFile/FileModal.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -234,15 +234,15 @@ export default function FileModal({
234234
</Title>
235235
<Combobox
236236
zIndex={90000}
237+
withinPortal={false}
237238
store={tagsCombobox}
238239
onOptionSubmit={handleValueSelect}
239-
withinPortal={false}
240240
>
241241
<Combobox.DropdownTarget>
242242
<PillsInput
243243
onBlur={() => triggerSave()}
244244
pointer
245-
onClick={() => tagsCombobox.toggleDropdown()}
245+
onClick={() => tagsCombobox.openDropdown()}
246246
>
247247
<Pill.Group>
248248
{values.length > 0 ? (
@@ -254,9 +254,14 @@ export default function FileModal({
254254
<Combobox.EventsTarget>
255255
<PillsInput.Field
256256
type='hidden'
257+
onFocus={() => tagsCombobox.openDropdown()}
257258
onBlur={() => tagsCombobox.closeDropdown()}
258259
onKeyDown={(event) => {
259-
if (event.key === 'Backspace') {
260+
if (
261+
event.key === 'Backspace' &&
262+
value.length > 0 &&
263+
event.currentTarget.value === ''
264+
) {
260265
event.preventDefault();
261266
handleValueRemove(value[value.length - 1]);
262267
}
@@ -285,9 +290,7 @@ export default function FileModal({
285290
</Combobox.Option>
286291
))
287292
) : (
288-
<Combobox.Option value='no-tags' disabled>
289-
No tags found, create one outside of this menu.
290-
</Combobox.Option>
293+
<Combobox.Empty>No tags found, create one outside of this menu.</Combobox.Empty>
291294
)}
292295
</Combobox.Options>
293296
</Combobox.Dropdown>
@@ -310,8 +313,8 @@ export default function FileModal({
310313
</Button>
311314
) : (
312315
<Combobox
313-
store={folderCombobox}
314316
withinPortal={false}
317+
store={folderCombobox}
315318
onOptionSubmit={(value) => handleAdd(value)}
316319
>
317320
<Combobox.Target>

src/components/render/code/HighlightCode.theme.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
.theme {
2828
color: var(--_color);
2929
background: var(--_background);
30+
display: block;
3031

3132
.hljs-comment,
3233
.hljs-quote {

src/components/render/code/HighlightCode.tsx

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ActionIcon, Button, CopyButton, Paper, ScrollArea, Text, useMantineTheme } from '@mantine/core';
2-
import { IconCheck, IconClipboardCopy, IconChevronDown, IconChevronUp } from '@tabler/icons-react';
3-
import { useEffect, useState } from 'react';
2+
import { IconCheck, IconChevronDown, IconChevronUp, IconClipboardCopy } from '@tabler/icons-react';
3+
import type { HLJSApi } from 'highlight.js';
4+
import { useEffect, useMemo, useState } from 'react';
5+
import { FixedSizeList as List } from 'react-window';
46

57
import './HighlightCode.theme.scss';
6-
import { type HLJSApi } from 'highlight.js';
78

89
export default function HighlightCode({ language, code }: { language: string; code: string }) {
910
const theme = useMantineTheme();
@@ -14,15 +15,56 @@ export default function HighlightCode({ language, code }: { language: string; co
1415
import('highlight.js').then((mod) => setHljs(mod.default || mod));
1516
}, []);
1617

17-
const lines = code.split('\n');
18-
const lineNumbers = lines.map((_, i) => i + 1);
19-
const displayLines = expanded ? lines : lines.slice(0, 50);
20-
const displayLineNumbers = expanded ? lineNumbers : lineNumbers.slice(0, 50);
18+
const lines = useMemo(() => code.split('\n'), [code]);
19+
const visible = expanded ? lines.length : Math.min(lines.length, 50);
20+
const expandable = lines.length > 50;
2121

22-
let lang = language;
23-
if (!hljs || !hljs.getLanguage(lang)) {
24-
lang = 'text';
25-
}
22+
const lang = useMemo(() => {
23+
if (!hljs) return 'plaintext';
24+
if (hljs.getLanguage(language)) return language;
25+
26+
return 'plaintext';
27+
}, [hljs, language]);
28+
29+
const hlLines = useMemo(() => {
30+
if (!hljs) return lines;
31+
32+
return lines.map(
33+
(line) =>
34+
hljs.highlight(line, {
35+
language: lang,
36+
}).value,
37+
);
38+
}, [lines, hljs, lang]);
39+
40+
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
41+
<div
42+
style={{
43+
...style,
44+
display: 'flex',
45+
alignItems: 'flex-start',
46+
whiteSpace: 'pre',
47+
fontFamily: 'monospace',
48+
fontSize: '0.8rem',
49+
}}
50+
>
51+
<Text
52+
component='span'
53+
c='dimmed'
54+
mr='md'
55+
style={{
56+
userSelect: 'none',
57+
width: 40,
58+
textAlign: 'right',
59+
flexShrink: 0,
60+
}}
61+
>
62+
{index + 1}
63+
</Text>
64+
65+
<code className='theme hljs' style={{ flex: 1 }} dangerouslySetInnerHTML={{ __html: hlLines[index] }} />
66+
</div>
67+
);
2668

2769
return (
2870
<Paper withBorder p='xs' my='md' pos='relative'>
@@ -44,37 +86,17 @@ export default function HighlightCode({ language, code }: { language: string; co
4486
)}
4587
</CopyButton>
4688

47-
<ScrollArea type='auto' dir='ltr' offsetScrollbars={false}>
48-
<pre style={{ margin: 0, whiteSpace: 'pre', overflowX: 'auto' }} className='theme'>
49-
<code className='theme'>
50-
{displayLines.map((line, i) => (
51-
<div key={i}>
52-
<Text
53-
component='span'
54-
size='sm'
55-
c='dimmed'
56-
mr='md'
57-
style={{ userSelect: 'none', fontFamily: 'monospace' }}
58-
>
59-
{displayLineNumbers[i]}
60-
</Text>
61-
<span
62-
className='line'
63-
dangerouslySetInnerHTML={{
64-
__html: lang === 'none' || !hljs ? line : hljs.highlight(line, { language: lang }).value,
65-
}}
66-
/>
67-
</div>
68-
))}
69-
</code>
70-
</pre>
89+
<ScrollArea type='auto' offsetScrollbars={false} style={{ maxHeight: 400 }}>
90+
<List height={400} width='100%' itemCount={visible} itemSize={20} overscanCount={10}>
91+
{Row}
92+
</List>
7193
</ScrollArea>
7294

73-
{lines.length > 50 && (
95+
{expandable && (
7496
<Button
75-
variant='outline'
97+
variant='light'
7698
size='compact-sm'
77-
onClick={() => setExpanded(!expanded)}
99+
onClick={() => setExpanded((e) => !e)}
78100
leftSection={expanded ? <IconChevronUp size='1rem' /> : <IconChevronDown size='1rem' />}
79101
style={{ position: 'absolute', bottom: '0.5rem', right: '0.5rem' }}
80102
>

0 commit comments

Comments
 (0)