perf: full-stack performance optimization

Frontend:
- Debounce queryStore tab persistence (remove flush:sync deep watcher)
- Eliminate unnecessary object spreading in connectionStore error handling
- Use in-place delete for completion cache invalidation
- Deduplicate schema cache prefix API calls
- Split echarts, reka-ui, marked, @codemirror/commands into separate chunks
- Lazy-load xlsxExport and markdownTable in DataGrid
- Lazy-load marked parser in UpdateDialog

Backend:
- Replace tokio::sync::Mutex with RwLock for AppState connections/configs
  (60+ concurrent reads no longer block each other)
- Parallelize PostgreSQL DDL metadata queries with tokio::try_join!
This commit is contained in:
t8y2
2026-05-10 23:23:22 +08:00
parent dbfe69572e
commit bafb424b23
13 changed files with 126 additions and 99 deletions
+2 -2
View File
@@ -74,8 +74,6 @@ import {
} from "@/lib/gridSelection";
import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql";
import { buildDataGridRollbackStatements, buildDataGridSaveStatements, formatGridSqlLiteral } from "@/lib/dataGridSql";
import { formatMarkdownTable } from "@/lib/markdownTable";
import { buildXlsxWorkbook } from "@/lib/xlsxExport";
import {
matchesRowStatusFilter,
rowStatusFilterAfterAddingRow,
@@ -2019,6 +2017,7 @@ async function exportMarkdown() {
try {
const cols = props.result.columns;
const visibleRows = displayItems.value.map((item) => item.data);
const { formatMarkdownTable } = await import("@/lib/markdownTable");
const md = formatMarkdownTable({ columns: cols, rows: visibleRows });
if (await saveFileContent(md, "export.md", "Markdown", "md")) {
toast(t("grid.exported"));
@@ -2030,6 +2029,7 @@ async function exportMarkdown() {
async function exportXlsx() {
try {
const { buildXlsxWorkbook } = await import("@/lib/xlsxExport");
const workbook = buildXlsxWorkbook({
sheetName: props.tableMeta?.tableName || "Export",
columns: props.result.columns,
+15 -7
View File
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed } from "vue";
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Marked } from "marked";
import { Loader2 } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -27,12 +26,21 @@ const emit = defineEmits<{
const { t } = useI18n();
const isDesktop = isTauriRuntime();
const marked = new Marked({ breaks: true, gfm: true });
const renderedNotes = ref("");
const renderedNotes = computed(() => {
if (!props.updateInfo?.release_notes) return "";
return marked.parse(props.updateInfo.release_notes) as string;
});
watch(
() => props.updateInfo?.release_notes,
async (notes) => {
if (!notes) {
renderedNotes.value = "";
return;
}
const { Marked } = await import("marked");
const marked = new Marked({ breaks: true, gfm: true });
renderedNotes.value = marked.parse(notes) as string;
},
{ immediate: true },
);
</script>
<template>
+17 -15
View File
@@ -121,17 +121,12 @@ export const useConnectionStore = defineStore("connection", () => {
}
function setConnectionError(connectionId: string, message: string) {
connectionErrors.value = {
...connectionErrors.value,
[connectionId]: message,
};
connectionErrors.value[connectionId] = message;
}
function clearConnectionError(connectionId: string) {
if (!connectionErrors.value[connectionId]) return;
const next = { ...connectionErrors.value };
delete next[connectionId];
connectionErrors.value = next;
delete connectionErrors.value[connectionId];
}
function recordConnectionError(connectionId: string, error: unknown): string {
@@ -323,8 +318,15 @@ export const useConnectionStore = defineStore("connection", () => {
loadedTreeNodeChildrenIds.value.delete(id);
}
}
api.deleteSchemaCachePrefix(`${prefix}:`).catch(() => undefined);
api.deleteSchemaCachePrefix(`${schemaCacheKey(prefix)}:`).catch(() => undefined);
const rawPrefix = `${prefix}:`;
const encodedPrefix = `${schemaCacheKey(prefix)}:`;
if (rawPrefix === encodedPrefix) {
api.deleteSchemaCachePrefix(rawPrefix).catch(() => undefined);
} else {
Promise.all([api.deleteSchemaCachePrefix(rawPrefix), api.deleteSchemaCachePrefix(encodedPrefix)]).catch(
() => undefined,
);
}
}
function schemaCachePrefixForNode(node: TreeNode): string | null {
@@ -399,12 +401,12 @@ export const useConnectionStore = defineStore("connection", () => {
function invalidateCompletionCache(connectionId: string) {
const cachePrefix = `${connectionId}:`;
completionTablesCache.value = Object.fromEntries(
Object.entries(completionTablesCache.value).filter(([key]) => !key.startsWith(cachePrefix)),
);
completionColumnsCache.value = Object.fromEntries(
Object.entries(completionColumnsCache.value).filter(([key]) => !key.startsWith(cachePrefix)),
);
for (const key of Object.keys(completionTablesCache.value)) {
if (key.startsWith(cachePrefix)) delete completionTablesCache.value[key];
}
for (const key of Object.keys(completionColumnsCache.value)) {
if (key.startsWith(cachePrefix)) delete completionColumnsCache.value[key];
}
}
async function removeConnection(id: string) {
+12 -1
View File
@@ -39,7 +39,18 @@ export const useQueryStore = defineStore("query", () => {
const activeTabId = ref<string | null>(restored.activeTabId);
const MAX_CACHED_RESULTS = 10;
watch([tabs, activeTabId], () => saveTabs(tabs.value, activeTabId.value), { deep: true, flush: "sync" });
let _persistTimer: ReturnType<typeof setTimeout> | null = null;
watch(
[tabs, activeTabId],
() => {
if (_persistTimer) clearTimeout(_persistTimer);
_persistTimer = setTimeout(() => {
saveTabs(tabs.value, activeTabId.value);
_persistTimer = null;
}, 300);
},
{ deep: true },
);
function findTabByTitle(connectionId: string, database: string, title: string) {
return tabs.value.find((t) => t.connectionId === connectionId && t.database === database && t.title === title);