From 2402c6f0ef85f0099d295a8b05b415dea26eb9ec Mon Sep 17 00:00:00 2001 From: diced Date: Thu, 23 Oct 2025 21:51:37 -0700 Subject: [PATCH] fix: performance issues with code renderer (#911) --- docker-compose.dev.yml | 2 +- package.json | 2 + pnpm-lock.yaml | 32 ++++++ .../file/DashboardFile/FileModal.tsx | 17 ++-- .../render/code/HighlightCode.theme.scss | 1 + src/components/render/code/HighlightCode.tsx | 98 ++++++++++++------- 6 files changed, 106 insertions(+), 46 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7176ad5c..6f434c49 100755 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,6 +1,6 @@ services: postgres: - image: postgres:15 + image: postgres:16 restart: unless-stopped environment: - POSTGRES_USER=postgres diff --git a/package.json b/package.json index b15d6c02..726692f5 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0d9e853..b6c92e55 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/components/file/DashboardFile/FileModal.tsx b/src/components/file/DashboardFile/FileModal.tsx index 25b41509..26557e9d 100755 --- a/src/components/file/DashboardFile/FileModal.tsx +++ b/src/components/file/DashboardFile/FileModal.tsx @@ -234,15 +234,15 @@ export default function FileModal({ triggerSave()} pointer - onClick={() => tagsCombobox.toggleDropdown()} + onClick={() => tagsCombobox.openDropdown()} > {values.length > 0 ? ( @@ -254,9 +254,14 @@ export default function FileModal({ 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({ )) ) : ( - - No tags found, create one outside of this menu. - + No tags found, create one outside of this menu. )} @@ -310,8 +313,8 @@ export default function FileModal({ ) : ( handleAdd(value)} > diff --git a/src/components/render/code/HighlightCode.theme.scss b/src/components/render/code/HighlightCode.theme.scss index 33e82ba6..6dd783f4 100755 --- a/src/components/render/code/HighlightCode.theme.scss +++ b/src/components/render/code/HighlightCode.theme.scss @@ -27,6 +27,7 @@ .theme { color: var(--_color); background: var(--_background); + display: block; .hljs-comment, .hljs-quote { diff --git a/src/components/render/code/HighlightCode.tsx b/src/components/render/code/HighlightCode.tsx index 3d2c7a3b..fd5fab15 100755 --- a/src/components/render/code/HighlightCode.tsx +++ b/src/components/render/code/HighlightCode.tsx @@ -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 }) => ( +
+ + {index + 1} + + + +
+ ); return ( @@ -44,37 +86,17 @@ export default function HighlightCode({ language, code }: { language: string; co )} - -
-          
-            {displayLines.map((line, i) => (
-              
- - {displayLineNumbers[i]} - - -
- ))} -
-
+ + + {Row} + - {lines.length > 50 && ( + {expandable && (