️ perf(webview): 降低首屏加载与 WebView2 内存占用

- Monaco Editor 改为首次使用时按需初始化
- AI 面板改为懒加载,延后加载 Markdown 和图表渲染依赖
- 增加 Windows 低内存视觉模式,支持关闭透明 WebView 和 Acrylic
- 补充低内存启动说明与模式解析测试
This commit is contained in:
Syngnat
2026-05-16 11:18:48 +08:00
parent a5be4cc3ae
commit cfbfda4de3
18 changed files with 184 additions and 40 deletions

View File

@@ -5,7 +5,7 @@ import { Table, message, Input, Button, Dropdown, MenuProps, Form, Pagination, S
import dayjs from 'dayjs';
import type { SortOrder, ColumnType } from 'antd/es/table/interface';
import { ReloadOutlined, ImportOutlined, ExportOutlined, DownOutlined, PlusOutlined, DeleteOutlined, SaveOutlined, UndoOutlined, FilterOutlined, CloseOutlined, ConsoleSqlOutlined, FileTextOutlined, CopyOutlined, ClearOutlined, EditOutlined, VerticalAlignBottomOutlined, LeftOutlined, RightOutlined, RobotOutlined, SearchOutlined } from '@ant-design/icons';
import Editor from '@monaco-editor/react';
import Editor from './MonacoEditor';
import {
DndContext,
DragEndEvent,

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import Editor from '@monaco-editor/react';
import Editor from './MonacoEditor';
import { Spin, Alert } from 'antd';
import { TabData } from '../types';
import { useStore } from '../store';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Editor, { type BeforeMount, type OnMount } from "@monaco-editor/react";
import Editor, { type BeforeMount, type OnMount } from "./MonacoEditor";
import {
Alert,
Button,

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import Editor from "@monaco-editor/react";
import Editor from "./MonacoEditor";
import {
Alert,
Button,

View File

@@ -0,0 +1,106 @@
import React, { useCallback, useEffect, useState } from 'react';
import Editor, { loader, type BeforeMount, type EditorProps } from '@monaco-editor/react';
export type { BeforeMount, OnMount } from '@monaco-editor/react';
let monacoConfiguredPromise: Promise<void> | null = null;
let transparentThemesRegistered = false;
const isTestRuntime = (): boolean => {
const env = (import.meta as unknown as { env?: Record<string, unknown> }).env || {};
return env.MODE === 'test' || env.VITEST === true || env.VITEST === 'true';
};
export const registerGonaviMonacoThemes: BeforeMount = (monaco) => {
if (transparentThemesRegistered) {
return;
}
monaco.editor.defineTheme('transparent-dark', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#00000000',
'editor.lineHighlightBackground': '#ffffff10',
'editorGutter.background': '#00000000',
'editorStickyScroll.background': '#1e1e1e',
'editorStickyScrollHover.background': '#2a2a2a',
},
});
monaco.editor.defineTheme('transparent-light', {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editor.background': '#00000000',
'editor.lineHighlightBackground': '#00000010',
'editorGutter.background': '#00000000',
'editorStickyScroll.background': '#ffffff',
'editorStickyScrollHover.background': '#f5f5f5',
},
});
transparentThemesRegistered = true;
};
const ensureMonacoConfigured = (): Promise<void> => {
if (isTestRuntime()) {
return Promise.resolve();
}
if (!monacoConfiguredPromise) {
monacoConfiguredPromise = import('monaco-editor/esm/nls.messages.zh-cn')
.then(() => import('monaco-editor'))
.then((monaco) => {
loader.config({ monaco });
});
}
return monacoConfiguredPromise;
};
const MonacoEditor: React.FC<EditorProps> = ({ beforeMount, loading, ...props }) => {
const [ready, setReady] = useState(isTestRuntime);
useEffect(() => {
let cancelled = false;
void ensureMonacoConfigured()
.then(() => {
if (!cancelled) {
setReady(true);
}
})
.catch((error) => {
console.error('Failed to configure Monaco Editor', error);
if (!cancelled) {
setReady(true);
}
});
return () => {
cancelled = true;
};
}, []);
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
registerGonaviMonacoThemes(monaco);
beforeMount?.(monaco);
}, [beforeMount]);
if (!ready) {
return (
<div
data-monaco-editor-loading="true"
style={{ height: props.height || '100%', width: props.width || '100%' }}
>
{loading || null}
</div>
);
}
return <Editor {...props} loading={loading} beforeMount={handleBeforeMount} />;
};
export default MonacoEditor;

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
import Editor, { OnMount } from '@monaco-editor/react';
import Editor, { type OnMount } from './MonacoEditor';
import { Button, message, Modal, Input, Form, Dropdown, MenuProps, Tooltip, Select, Tabs } from 'antd';
import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, CloseOutlined, StopOutlined, RobotOutlined } from '@ant-design/icons';
import { format } from 'sql-formatter';
@@ -924,7 +924,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
editorRef.current = editor;
monacoRef.current = monaco;
// 应用透明主题(主题已在 main.tsx 全局注册)
// 应用透明主题(主题由 MonacoEditor 包装组件按需注册)
monaco.editor.setTheme(darkMode ? 'transparent-dark' : 'transparent-light');
// 注册 AI 右键菜单操作

View File

@@ -3,7 +3,7 @@ import { Button, Space, message } from 'antd';
import { PlayCircleOutlined, ClearOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import Editor, { OnMount } from '@monaco-editor/react';
import Editor, { type OnMount } from './MonacoEditor';
interface RedisCommandEditorProps {
connectionId: string;

View File

@@ -5,7 +5,7 @@ import type { RadioChangeEvent } from 'antd';
import { ReloadOutlined, DeleteOutlined, PlusOutlined, EditOutlined, SearchOutlined, ClockCircleOutlined, CopyOutlined, FolderOpenOutlined, KeyOutlined, RightOutlined, DownOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import { RedisKeyInfo, RedisValue, StreamEntry } from '../types';
import Editor from '@monaco-editor/react';
import Editor from './MonacoEditor';
import type { DataNode } from 'antd/es/tree';
import {
blurToFilter,

View File

@@ -4,7 +4,7 @@ import { ReloadOutlined, SaveOutlined, PlusOutlined, DeleteOutlined, MenuOutline
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragOverlay } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import Editor, { loader } from '@monaco-editor/react';
import Editor from './MonacoEditor';
import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, TriggerDefinition } from '../types';
import { useStore } from '../store';
import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App';
@@ -460,7 +460,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
setCommentEditorValue('');
}, []);
// 透明 Monaco Editor 主题已在 main.tsx 全局注册(含 stickyScroll 不透明背景)
// 透明 Monaco Editor 主题由 MonacoEditor 包装组件按需注册(含 stickyScroll 不透明背景)
// 监听字段 Tab 容器高度,为所有 Tab 内表格计算 scroll.y
// 当 Tab 切换时,字段 Tab 被 display:none 导致 height=0跳过该次更新保持有效值

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import Editor, { type BeforeMount, type OnMount } from '@monaco-editor/react';
import Editor, { type BeforeMount, type OnMount } from './MonacoEditor';
interface TableDesignerSqlPreviewProps {
sql: string;

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import Editor from '@monaco-editor/react';
import Editor from './MonacoEditor';
import { Spin, Alert } from 'antd';
import { TabData } from '../types';
import { useStore } from '../store';
@@ -20,7 +20,7 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
const theme = useStore(state => state.theme);
const darkMode = theme === 'dark';
// 透明 Monaco Editor 主题已在 main.tsx 全局注册(含 stickyScroll 不透明背景)
// 透明 Monaco Editor 主题由 MonacoEditor 包装组件按需注册(含 stickyScroll 不透明背景)
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;