fix: performance issues with code renderer (#911)

This commit is contained in:
diced
2025-10-23 21:51:37 -07:00
parent 317e97e3a6
commit 2402c6f0ef
6 changed files with 106 additions and 46 deletions

View File

@@ -1,6 +1,6 @@
services:
postgres:
image: postgres:15
image: postgres:16
restart: unless-stopped
environment:
- POSTGRES_USER=postgres

View File

@@ -78,6 +78,7 @@
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.8.2",
"react-window": "1.8.11",
"remark-gfm": "^4.0.1",
"sharp": "^0.34.3",
"swr": "^2.3.6",
@@ -96,6 +97,7 @@
"@types/qrcode": "^1.5.5",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@types/react-window": "^1.8.8",
"@vitejs/plugin-react": "^5.0.2",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",

32
pnpm-lock.yaml generated
View File

@@ -179,6 +179,9 @@ importers:
react-router-dom:
specifier: ^7.8.2
version: 7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-window:
specifier: 1.8.11
version: 1.8.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
@@ -228,6 +231,9 @@ importers:
'@types/react-dom':
specifier: ^19.1.9
version: 19.1.9(@types/react@19.1.12)
'@types/react-window':
specifier: ^1.8.8
version: 1.8.8
'@vitejs/plugin-react':
specifier: ^5.0.2
version: 5.0.2(vite@7.1.4(@types/node@24.3.0)(jiti@2.5.1)(sass@1.92.0)(sugarss@5.0.1(postcss@8.5.6))(tsx@4.20.5))
@@ -2058,6 +2064,9 @@ packages:
peerDependencies:
'@types/react': ^19.0.0
'@types/react-window@1.8.8':
resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==}
'@types/react@19.1.12':
resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==}
@@ -3552,6 +3561,9 @@ packages:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
memoize-one@5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -4183,6 +4195,13 @@ packages:
react: '>=16.6.0'
react-dom: '>=16.6.0'
react-window@1.8.11:
resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==}
engines: {node: '>8.0.0'}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react@19.1.1:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
engines: {node: '>=0.10.0'}
@@ -7150,6 +7169,10 @@ snapshots:
dependencies:
'@types/react': 19.1.12
'@types/react-window@1.8.8':
dependencies:
'@types/react': 19.1.12
'@types/react@19.1.12':
dependencies:
csstype: 3.1.3
@@ -8961,6 +8984,8 @@ snapshots:
media-typer@0.3.0: {}
memoize-one@5.2.1: {}
merge2@1.4.1: {}
micromark-core-commonmark@2.0.3:
@@ -9693,6 +9718,13 @@ snapshots:
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
react-window@1.8.11(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
'@babel/runtime': 7.28.3
memoize-one: 5.2.1
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
react@19.1.1: {}
read-package-up@11.0.0:

View File

@@ -234,15 +234,15 @@ export default function FileModal({
</Title>
<Combobox
zIndex={90000}
withinPortal={false}
store={tagsCombobox}
onOptionSubmit={handleValueSelect}
withinPortal={false}
>
<Combobox.DropdownTarget>
<PillsInput
onBlur={() => triggerSave()}
pointer
onClick={() => tagsCombobox.toggleDropdown()}
onClick={() => tagsCombobox.openDropdown()}
>
<Pill.Group>
{values.length > 0 ? (
@@ -254,9 +254,14 @@ export default function FileModal({
<Combobox.EventsTarget>
<PillsInput.Field
type='hidden'
onFocus={() => tagsCombobox.openDropdown()}
onBlur={() => tagsCombobox.closeDropdown()}
onKeyDown={(event) => {
if (event.key === 'Backspace') {
if (
event.key === 'Backspace' &&
value.length > 0 &&
event.currentTarget.value === ''
) {
event.preventDefault();
handleValueRemove(value[value.length - 1]);
}
@@ -285,9 +290,7 @@ export default function FileModal({
</Combobox.Option>
))
) : (
<Combobox.Option value='no-tags' disabled>
No tags found, create one outside of this menu.
</Combobox.Option>
<Combobox.Empty>No tags found, create one outside of this menu.</Combobox.Empty>
)}
</Combobox.Options>
</Combobox.Dropdown>
@@ -310,8 +313,8 @@ export default function FileModal({
</Button>
) : (
<Combobox
store={folderCombobox}
withinPortal={false}
store={folderCombobox}
onOptionSubmit={(value) => handleAdd(value)}
>
<Combobox.Target>

View File

@@ -27,6 +27,7 @@
.theme {
color: var(--_color);
background: var(--_background);
display: block;
.hljs-comment,
.hljs-quote {

View File

@@ -1,9 +1,10 @@
import { ActionIcon, Button, CopyButton, Paper, ScrollArea, Text, useMantineTheme } from '@mantine/core';
import { IconCheck, IconClipboardCopy, IconChevronDown, IconChevronUp } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { IconCheck, IconChevronDown, IconChevronUp, IconClipboardCopy } from '@tabler/icons-react';
import type { HLJSApi } from 'highlight.js';
import { useEffect, useMemo, useState } from 'react';
import { FixedSizeList as List } from 'react-window';
import './HighlightCode.theme.scss';
import { type HLJSApi } from 'highlight.js';
export default function HighlightCode({ language, code }: { language: string; code: string }) {
const theme = useMantineTheme();
@@ -14,15 +15,56 @@ export default function HighlightCode({ language, code }: { language: string; co
import('highlight.js').then((mod) => setHljs(mod.default || mod));
}, []);
const lines = code.split('\n');
const lineNumbers = lines.map((_, i) => i + 1);
const displayLines = expanded ? lines : lines.slice(0, 50);
const displayLineNumbers = expanded ? lineNumbers : lineNumbers.slice(0, 50);
const lines = useMemo(() => code.split('\n'), [code]);
const visible = expanded ? lines.length : Math.min(lines.length, 50);
const expandable = lines.length > 50;
let lang = language;
if (!hljs || !hljs.getLanguage(lang)) {
lang = 'text';
}
const lang = useMemo(() => {
if (!hljs) return 'plaintext';
if (hljs.getLanguage(language)) return language;
return 'plaintext';
}, [hljs, language]);
const hlLines = useMemo(() => {
if (!hljs) return lines;
return lines.map(
(line) =>
hljs.highlight(line, {
language: lang,
}).value,
);
}, [lines, hljs, lang]);
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div
style={{
...style,
display: 'flex',
alignItems: 'flex-start',
whiteSpace: 'pre',
fontFamily: 'monospace',
fontSize: '0.8rem',
}}
>
<Text
component='span'
c='dimmed'
mr='md'
style={{
userSelect: 'none',
width: 40,
textAlign: 'right',
flexShrink: 0,
}}
>
{index + 1}
</Text>
<code className='theme hljs' style={{ flex: 1 }} dangerouslySetInnerHTML={{ __html: hlLines[index] }} />
</div>
);
return (
<Paper withBorder p='xs' my='md' pos='relative'>
@@ -44,37 +86,17 @@ export default function HighlightCode({ language, code }: { language: string; co
)}
</CopyButton>
<ScrollArea type='auto' dir='ltr' offsetScrollbars={false}>
<pre style={{ margin: 0, whiteSpace: 'pre', overflowX: 'auto' }} className='theme'>
<code className='theme'>
{displayLines.map((line, i) => (
<div key={i}>
<Text
component='span'
size='sm'
c='dimmed'
mr='md'
style={{ userSelect: 'none', fontFamily: 'monospace' }}
>
{displayLineNumbers[i]}
</Text>
<span
className='line'
dangerouslySetInnerHTML={{
__html: lang === 'none' || !hljs ? line : hljs.highlight(line, { language: lang }).value,
}}
/>
</div>
))}
</code>
</pre>
<ScrollArea type='auto' offsetScrollbars={false} style={{ maxHeight: 400 }}>
<List height={400} width='100%' itemCount={visible} itemSize={20} overscanCount={10}>
{Row}
</List>
</ScrollArea>
{lines.length > 50 && (
{expandable && (
<Button
variant='outline'
variant='light'
size='compact-sm'
onClick={() => setExpanded(!expanded)}
onClick={() => setExpanded((e) => !e)}
leftSection={expanded ? <IconChevronUp size='1rem' /> : <IconChevronDown size='1rem' />}
style={{ position: 'absolute', bottom: '0.5rem', right: '0.5rem' }}
>