️ 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

@@ -11,7 +11,6 @@ import ConnectionPackagePasswordModal from './components/ConnectionPackagePasswo
import DataSyncModal from './components/DataSyncModal';
import DriverManagerModal from './components/DriverManagerModal';
import LogPanel from './components/LogPanel';
import AIChatPanel from './components/AIChatPanel';
import AISettingsModal from './components/AISettingsModal';
import SecurityUpdateBanner from './components/SecurityUpdateBanner';
import SecurityUpdateIntroModal from './components/SecurityUpdateIntroModal';
@@ -85,6 +84,8 @@ import {
import { ApplyDataRootDirectory, GetDataRootDirectoryInfo, GetSavedConnections, OpenDataRootDirectory, SelectDataRootDirectory, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App';
import './App.css';
const AIChatPanel = React.lazy(() => import('./components/AIChatPanel'));
const { Sider, Content } = Layout;
const MIN_UI_SCALE = 0.8;
const MAX_UI_SCALE = 1.25;
@@ -3007,9 +3008,11 @@ function App() {
{renderAIEdgeHandle()}
</div>
)}
<AIChatPanel darkMode={darkMode} bgColor={bgContent} onClose={() => setAIPanelVisible(false)} onOpenSettings={() => {
handleOpenAISettings();
}} overlayTheme={overlayTheme} />
<React.Suspense fallback={<div style={{ width: 360, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Spin size="small" /></div>}>
<AIChatPanel darkMode={darkMode} bgColor={bgContent} onClose={() => setAIPanelVisible(false)} onOpenSettings={() => {
handleOpenAISettings();
}} overlayTheme={overlayTheme} />
</React.Suspense>
</div>
)}
</div>

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, ']]')}]`;

View File

@@ -9,14 +9,7 @@ import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn')
// 全局配置 Monaco Editor 使用本地打包的文件,避免从 CDN (jsdelivr) 加载。
// Windows WebView2 环境下访问外部 CDN 可能失败,导致编辑器一直显示 Loading。
// 中文语言包必须在 monaco-editor 主包之前导入,否则右键菜单等 UI 仍为英文。
import 'monaco-editor/esm/nls.messages.zh-cn'
import { loader } from '@monaco-editor/react'
import * as monaco from 'monaco-editor'
import { cloneBrowserMockValue, duplicateBrowserMockConnection, resolveBrowserMockSecretFlag } from './utils/browserMockConnections'
loader.config({ monaco })
if (typeof window !== 'undefined' && !(window as any).go) {
const mockConnections: any[] = [];
@@ -168,16 +161,6 @@ if (typeof window !== 'undefined' && !(window as any).go) {
}
};
}
// 全局注册透明主题,避免每个 Editor 组件 beforeMount 中重复定义
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' }
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />

View File

@@ -1,5 +1,7 @@
/// <reference types="vite/client" />
declare module 'monaco-editor/esm/nls.messages.zh-cn';
interface ImportMetaEnv {
readonly VITE_GONAVI_ENABLE_MAC_WINDOW_DIAGNOSTICS?: string;
}