From 355812e7cb3b8c568c3183c84e58302fd841f981 Mon Sep 17 00:00:00 2001 From: tesgth032 Date: Mon, 2 Mar 2026 20:31:22 +0800 Subject: [PATCH 01/30] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20CodeDisplay?= =?UTF-8?q?=20=E5=A4=A7=E5=86=85=E5=AE=B9=20Pretty=20=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 方案1(默认开):长内容 Pretty 视图降级为纯文本 textarea,避免 SyntaxHighlighter DOM 爆炸 - 方案3(可选开):虚拟窗口语法高亮(仅渲染可视切片),并提供 Plain/Virtual 视图切换 - JSON pretty / 行索引 / only-matches 搜索迁移到 Web Worker,支持进度与取消,失败自动回落 - 新增 CodeDisplay 相关 env 配置并更新 .env.example - 补充单元测试覆盖大内容策略与回退路径 --- .env.example | 17 + messages/en/dashboard.json | 8 + messages/ja/dashboard.json | 8 + messages/ru/dashboard.json | 8 + messages/zh-CN/dashboard.json | 8 + messages/zh-TW/dashboard.json | 8 + src/app/[locale]/layout.tsx | 4 +- src/app/providers.tsx | 29 +- .../ui/code-display-config-context.tsx | 27 + src/components/ui/code-display-config.ts | 154 +++ .../ui/code-display-matches-list.tsx | 131 ++ .../ui/code-display-plain-textarea.tsx | 40 + .../ui/code-display-virtual-highlighter.tsx | 309 +++++ .../ui/code-display-worker-client.ts | 432 ++++++ src/components/ui/code-display.tsx | 1231 +++++++++++++---- src/components/ui/code-display.worker.ts | 847 ++++++++++++ tests/unit/code-display-perf-ui.test.tsx | 323 +++++ 17 files changed, 3336 insertions(+), 248 deletions(-) create mode 100644 src/components/ui/code-display-config-context.tsx create mode 100644 src/components/ui/code-display-config.ts create mode 100644 src/components/ui/code-display-matches-list.tsx create mode 100644 src/components/ui/code-display-plain-textarea.tsx create mode 100644 src/components/ui/code-display-virtual-highlighter.tsx create mode 100644 src/components/ui/code-display-worker-client.ts create mode 100644 src/components/ui/code-display.worker.ts create mode 100644 tests/unit/code-display-perf-ui.test.tsx diff --git a/.env.example b/.env.example index a9216eebb..4f2b30080 100644 --- a/.env.example +++ b/.env.example @@ -79,6 +79,23 @@ STORE_SESSION_RESPONSE_BODY=true # 是否在 Redis 中存储会话响应 # - false:不存储响应体(注意:不影响本次请求处理;仅影响后续查看 response body) # 说明:该开关不影响内部统计读取响应体(tokens/费用统计、SSE 假 200 检测仍会进行) +# Dashboard CodeDisplay(长内容美化性能) +# 说明: +# - 方案1:长内容 Pretty 视图使用纯文本(textarea)展示,避免语法高亮 DOM 爆炸导致卡顿(默认开启)。 +# - 方案3:仅对可视窗口做语法高亮(虚拟化高亮)。默认关闭,需要显式开启;开启后可在 UI 切换“纯文本/虚拟高亮”。 +CCH_CODEDISPLAY_LARGE_PLAIN=true +CCH_CODEDISPLAY_VIRTUAL_HIGHLIGHT=false + +# 可选调参(一般无需修改) +CCH_CODEDISPLAY_WORKER_ENABLE=true +CCH_CODEDISPLAY_PERF_DEBUG=false +CCH_CODEDISPLAY_HIGHLIGHT_MAX_CHARS=30000 +CCH_CODEDISPLAY_VIRTUAL_OVERSCAN_LINES=50 +CCH_CODEDISPLAY_VIRTUAL_CONTEXT_LINES=50 +CCH_CODEDISPLAY_VIRTUAL_LINE_HEIGHT_PX=18 +CCH_CODEDISPLAY_MAX_PRETTY_OUTPUT_BYTES=20000000 +CCH_CODEDISPLAY_MAX_LINE_INDEX_LINES=200000 + # 熔断器配置 # 功能说明:控制网络错误是否计入熔断器失败计数 # - false (默认):网络错误(DNS 解析失败、连接超时、代理连接失败等)不计入熔断器,仅供应商错误(4xx/5xx HTTP 响应)计入 diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 15195b0c5..eb01f1f9f 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -568,7 +568,12 @@ "codeDisplay": { "raw": "Raw", "pretty": "Pretty", + "prettyWorking": "Formatting... {percent}%", + "viewPlain": "Plain", + "viewVirtual": "Virtual highlight", "searchPlaceholder": "Search", + "searchWorking": "Searching... {percent}%", + "cancel": "Cancel", "expand": "Expand", "collapse": "Collapse", "themeAuto": "Auto", @@ -582,6 +587,9 @@ "pageInfo": "Page {page} / {total}", "sseEvent": "Event", "sseData": "Data", + "virtual": { + "indexWorking": "Indexing... {percent}%" + }, "hardLimit": { "title": "Content too large", "size": "Size: {sizeMB} MB ({sizeBytes} bytes)", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 1555e52af..a8d1240f6 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -568,7 +568,12 @@ "codeDisplay": { "raw": "Raw", "pretty": "Pretty", + "prettyWorking": "整形中... {percent}%", + "viewPlain": "テキスト", + "viewVirtual": "仮想ハイライト", "searchPlaceholder": "検索", + "searchWorking": "検索中... {percent}%", + "cancel": "キャンセル", "expand": "展開", "collapse": "折りたたむ", "themeAuto": "自動", @@ -582,6 +587,9 @@ "pageInfo": "{page} / {total} ページ", "sseEvent": "イベント", "sseData": "データ", + "virtual": { + "indexWorking": "索引作成中... {percent}%" + }, "hardLimit": { "title": "コンテンツが大きすぎます", "size": "サイズ: {sizeMB} MB ({sizeBytes} bytes)", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 64a2c8211..2b16a9380 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -568,7 +568,12 @@ "codeDisplay": { "raw": "Сырой", "pretty": "Форматированный", + "prettyWorking": "Форматирование... {percent}%", + "viewPlain": "Текст", + "viewVirtual": "Виртуальная подсветка", "searchPlaceholder": "Поиск", + "searchWorking": "Поиск... {percent}%", + "cancel": "Отмена", "expand": "Развернуть", "collapse": "Свернуть", "themeAuto": "Авто", @@ -582,6 +587,9 @@ "pageInfo": "Страница {page} / {total}", "sseEvent": "Событие", "sseData": "Данные", + "virtual": { + "indexWorking": "Индексация... {percent}%" + }, "hardLimit": { "title": "Содержимое слишком большое", "size": "Размер: {sizeMB} MB ({sizeBytes} bytes)", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index 6743b6857..8615ff3ca 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -568,7 +568,12 @@ "codeDisplay": { "raw": "原始", "pretty": "美化", + "prettyWorking": "美化中... {percent}%", + "viewPlain": "纯文本", + "viewVirtual": "虚拟高亮", "searchPlaceholder": "搜索", + "searchWorking": "搜索中... {percent}%", + "cancel": "取消", "expand": "展开", "collapse": "收起", "themeAuto": "跟随系统", @@ -582,6 +587,9 @@ "pageInfo": "第 {page} / {total} 页", "sseEvent": "事件", "sseData": "数据", + "virtual": { + "indexWorking": "建立索引中... {percent}%" + }, "hardLimit": { "title": "内容过大", "size": "大小:{sizeMB} MB({sizeBytes} 字节)", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index e28e8eb07..5236bd5bf 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -568,7 +568,12 @@ "codeDisplay": { "raw": "原始內容", "pretty": "格式化", + "prettyWorking": "格式化中... {percent}%", + "viewPlain": "純文字", + "viewVirtual": "虛擬高亮", "searchPlaceholder": "搜尋", + "searchWorking": "搜尋中... {percent}%", + "cancel": "取消", "expand": "展開", "collapse": "摺疊", "themeAuto": "跟隨系統", @@ -582,6 +587,9 @@ "pageInfo": "第 {page} / {total} 頁", "sseEvent": "事件類型", "sseData": "資料", + "virtual": { + "indexWorking": "建立索引中... {percent}%" + }, "hardLimit": { "title": "內容過大", "size": "大小:{sizeMB} MB({sizeBytes} 位元組)", diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index a7c6c18d5..b521e0e97 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -4,6 +4,7 @@ import { notFound } from "next/navigation"; import { NextIntlClientProvider } from "next-intl"; import { getMessages } from "next-intl/server"; import { Footer } from "@/components/customs/footer"; +import { parseCodeDisplayConfigFromEnv } from "@/components/ui/code-display-config"; import { Toaster } from "@/components/ui/sonner"; import { type Locale, locales } from "@/i18n/config"; import { logger } from "@/lib/logger"; @@ -72,6 +73,7 @@ export default async function RootLayout({ // Load translation messages const messages = await getMessages(); const timeZone = await resolveSystemTimezone(); + const codeDisplayConfig = parseCodeDisplayConfigFromEnv(process.env); // Create a stable `now` timestamp to avoid SSR/CSR hydration mismatch for relative time const now = new Date(); @@ -79,7 +81,7 @@ export default async function RootLayout({ - +
{children}