perf(desktop): defer startup dialog chunks

This commit is contained in:
t8y2 2026-05-18 22:57:51 +08:00
parent b16393f68c
commit c29bd98d94
3 changed files with 66 additions and 10 deletions

View File

@ -1,10 +1,8 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
import { ref, computed, watch, onMounted, onUnmounted, nextTick, defineAsyncComponent } from "vue";
import { useI18n } from "vue-i18n";
import { invoke } from "@tauri-apps/api/core";
import { TooltipProvider } from "@/components/ui/tooltip";
import AiAssistant from "@/components/editor/AiAssistant.vue";
import QueryHistory from "@/components/editor/QueryHistory.vue";
import AppToolbar from "@/components/layout/AppToolbar.vue";
import AppTabBar from "@/components/layout/AppTabBar.vue";
import AppSidebar from "@/components/layout/AppSidebar.vue";
@ -12,9 +10,6 @@ import EditorToolbar from "@/components/layout/EditorToolbar.vue";
import ContentArea from "@/components/layout/ContentArea.vue";
import AppDialogs from "@/components/layout/AppDialogs.vue";
import WelcomeScreen from "@/components/layout/WelcomeScreen.vue";
import DriverStorePage from "@/components/config/DriverStoreDialog.vue";
import UpdateDialog from "@/components/layout/UpdateDialog.vue";
import LoginPage from "@/components/auth/LoginPage.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
@ -56,6 +51,17 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import type { HistoryEntry } from "@/lib/tauri";
import type { AiAction } from "@/lib/ai";
const AiAssistant = defineAsyncComponent(() => import("@/components/editor/AiAssistant.vue"));
const QueryHistory = defineAsyncComponent(() => import("@/components/editor/QueryHistory.vue"));
const DriverStorePage = defineAsyncComponent(() => import("@/components/config/DriverStoreDialog.vue"));
const UpdateDialog = defineAsyncComponent(() => import("@/components/layout/UpdateDialog.vue"));
const LoginPage = defineAsyncComponent(() => import("@/components/auth/LoginPage.vue"));
type AiAssistantHandle = {
triggerAction: (action: AiAction, instruction?: string) => void;
};
const { t } = useI18n();
const connectionStore = useConnectionStore();
@ -94,9 +100,10 @@ const showDriverStore = ref(false);
const agentDriverUpdateCount = ref(0);
const showHistory = ref(false);
const showAiPanel = ref(localStorage.getItem("dbx-ai-panel-open") !== "false");
const aiPanelReady = ref(false);
const { sidebarWidth, aiPanelWidth, historyWidth, startSidebarResize, startAiPanelResize, startHistoryResize } =
usePanelResize();
const aiAssistantRef = ref<InstanceType<typeof AiAssistant> | null>(null);
const aiAssistantRef = ref<AiAssistantHandle | null>(null);
const appSidebarRef = ref<InstanceType<typeof AppSidebar> | null>(null);
const contentAreaRef = ref<InstanceType<typeof ContentArea> | null>(null);
@ -629,6 +636,9 @@ function openDriverStoreFromEvent() {
onMounted(async () => {
console.log("[STARTUP] onMounted begin");
const mountStart = performance.now();
requestAnimationFrame(() => {
aiPanelReady.value = true;
});
applyTheme();
window.addEventListener("keydown", handleKeydown, true);
window.addEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
@ -857,6 +867,7 @@ onUnmounted(() => {
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startAiPanelResize" />
<div class="h-full min-h-0 overflow-hidden">
<AiAssistant
v-if="aiPanelReady"
ref="aiAssistantRef"
:tab="activeTab"
:connection="activeConnection"
@ -910,6 +921,7 @@ onUnmounted(() => {
@open-database-search-target="openDatabaseSearchTarget"
/>
<UpdateDialog
v-if="showUpdateDialog"
v-model:open="showUpdateDialog"
:update-info="updateInfo"
:update-check-message="updateCheckMessage"

View File

@ -3,9 +3,9 @@ import { computed, watch, defineAsyncComponent } from "vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import ConnectionDialog from "@/components/connection/ConnectionDialog.vue";
import EditorSettingsDialog from "@/components/editor/EditorSettingsDialog.vue";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
const ConnectionDialog = defineAsyncComponent(() => import("@/components/connection/ConnectionDialog.vue"));
const EditorSettingsDialog = defineAsyncComponent(() => import("@/components/editor/EditorSettingsDialog.vue"));
const DangerConfirmDialog = defineAsyncComponent(() => import("@/components/editor/DangerConfirmDialog.vue"));
const DataTransferDialog = defineAsyncComponent(() => import("@/components/transfer/DataTransferDialog.vue"));
const SchemaDiffDialog = defineAsyncComponent(() => import("@/components/diff/SchemaDiffDialog.vue"));
const DataCompareDialog = defineAsyncComponent(() => import("@/components/diff/DataCompareDialog.vue"));
@ -95,6 +95,7 @@ watch(
<template>
<ConnectionDialog
v-if="showConnectionDialog || editConfig"
:open="showConnectionDialog"
:edit-config="editConfig"
@update:open="emit('update:showConnectionDialog', $event)"
@ -104,29 +105,34 @@ watch(
@open-driver-store="emit('openDriverStore')"
/>
<EditorSettingsDialog
v-if="showSettingsDialog"
:open="showSettingsDialog"
:initial-tab="settingsInitialTab || 'editor'"
:app-version="appVersion"
@update:open="emit('update:showSettingsDialog', $event)"
/>
<DangerConfirmDialog
v-if="showDangerDialog"
:open="showDangerDialog"
:sql="dangerSql"
@update:open="emit('update:showDangerDialog', $event)"
@confirm="emit('dangerConfirm')"
/>
<DataTransferDialog
v-if="dialogs.showTransferDialog.value"
v-model:open="dialogs.showTransferDialog.value"
:prefill-connection-id="dialogs.transferPrefillConnectionId.value"
:prefill-database="dialogs.transferPrefillDatabase.value"
/>
<SchemaDiffDialog
v-if="dialogs.showSchemaDiffDialog.value"
v-model:open="dialogs.showSchemaDiffDialog.value"
:prefill-connection-id="dialogs.schemaDiffPrefillConnectionId.value"
:prefill-database="dialogs.schemaDiffPrefillDatabase.value"
:prefill-schema="dialogs.schemaDiffPrefillSchema.value"
/>
<DataCompareDialog
v-if="dialogs.showDataCompareDialog.value"
v-model:open="dialogs.showDataCompareDialog.value"
:prefill-connection-id="dialogs.dataComparePrefillConnectionId.value"
:prefill-database="dialogs.dataComparePrefillDatabase.value"
@ -134,11 +140,13 @@ watch(
:prefill-table="dialogs.dataComparePrefillTable.value"
/>
<SqlFileExecutionDialog
v-if="dialogs.showSqlFileDialog.value"
v-model:open="dialogs.showSqlFileDialog.value"
:prefill-connection-id="dialogs.sqlFilePrefillConnectionId.value"
:prefill-database="dialogs.sqlFilePrefillDatabase.value"
/>
<SchemaDiagramDialog
v-if="dialogs.showDiagramDialog.value"
v-model:open="dialogs.showDiagramDialog.value"
:prefill-connection-id="dialogs.diagramPrefillConnectionId.value"
:prefill-database="dialogs.diagramPrefillDatabase.value"
@ -146,6 +154,7 @@ watch(
:focus-table-name="dialogs.diagramFocusTableName.value"
/>
<TableImportDialog
v-if="dialogs.showTableImportDialog.value"
v-model:open="dialogs.showTableImportDialog.value"
:prefill-connection-id="dialogs.tableImportPrefillConnectionId.value"
:prefill-database="dialogs.tableImportPrefillDatabase.value"
@ -153,6 +162,7 @@ watch(
:prefill-table="dialogs.tableImportPrefillTable.value"
/>
<TableStructureEditorDialog
v-if="dialogs.showStructureEditorDialog.value"
v-model:open="dialogs.showStructureEditorDialog.value"
:prefill-connection-id="dialogs.structurePrefillConnectionId.value"
:prefill-database="dialogs.structurePrefillDatabase.value"
@ -161,6 +171,7 @@ watch(
@saved="emit('structureEditorSaved')"
/>
<FieldLineageDialog
v-if="dialogs.showFieldLineageDialog.value"
v-model:open="dialogs.showFieldLineageDialog.value"
:prefill-connection-id="dialogs.lineagePrefillConnectionId.value"
:prefill-database="dialogs.lineagePrefillDatabase.value"
@ -170,6 +181,7 @@ watch(
@open-target="emit('openLineageTarget', $event)"
/>
<DatabaseSearchDialog
v-if="dialogs.showDatabaseSearchDialog.value"
v-model:open="dialogs.showDatabaseSearchDialog.value"
:prefill-connection-id="dialogs.databaseSearchPrefillConnectionId.value"
:prefill-database="dialogs.databaseSearchPrefillDatabase.value"
@ -177,6 +189,7 @@ watch(
@open-target="emit('openDatabaseSearchTarget', $event)"
/>
<DatabaseExportDialog
v-if="dialogs.showDatabaseExportDialog.value"
v-model:open="dialogs.showDatabaseExportDialog.value"
:prefill-connection-id="dialogs.databaseExportPrefillConnectionId.value"
:prefill-database="dialogs.databaseExportPrefillDatabase.value"
@ -184,6 +197,7 @@ watch(
:prefill-table="dialogs.databaseExportPrefillTable.value"
/>
<ConfigPassphraseDialog
v-if="dialogs.showConfigPassphraseDialog.value"
v-model:open="dialogs.showConfigPassphraseDialog.value"
:mode="dialogs.configPassphraseMode.value"
:external-error="dialogs.configPassphraseError.value"

View File

@ -0,0 +1,30 @@
import { readFileSync } from "node:fs";
import assert from "node:assert/strict";
import test from "node:test";
const appSource = readFileSync("apps/desktop/src/App.vue", "utf8");
const appDialogsSource = readFileSync("apps/desktop/src/components/layout/AppDialogs.vue", "utf8");
test("app defers cold-start side panels and modal pages behind async components", () => {
assert.match(appSource, /defineAsyncComponent/);
for (const component of ["AiAssistant", "QueryHistory", "DriverStorePage", "UpdateDialog", "LoginPage"]) {
assert.doesNotMatch(appSource, new RegExp(`import ${component} from`));
assert.match(appSource, new RegExp(`const ${component} = defineAsyncComponent`));
}
});
test("app dialogs keep non-primary dialogs out of the startup chunk", () => {
for (const component of ["ConnectionDialog", "EditorSettingsDialog", "DangerConfirmDialog"]) {
assert.doesNotMatch(appDialogsSource, new RegExp(`import ${component} from`));
assert.match(appDialogsSource, new RegExp(`const ${component} = defineAsyncComponent`));
}
});
test("app dialogs only render async dialogs when their open state needs them", () => {
assert.match(appDialogsSource, /<ConnectionDialog\s+v-if="showConnectionDialog \|\| editConfig"/);
assert.match(appDialogsSource, /<EditorSettingsDialog\s+v-if="showSettingsDialog"/);
assert.match(appDialogsSource, /<DangerConfirmDialog\s+v-if="showDangerDialog"/);
assert.match(appDialogsSource, /<DataTransferDialog\s+v-if="dialogs\.showTransferDialog\.value"/);
assert.match(appDialogsSource, /<SchemaDiagramDialog\s+v-if="dialogs\.showDiagramDialog\.value"/);
assert.match(appDialogsSource, /<DatabaseExportDialog\s+v-if="dialogs\.showDatabaseExportDialog\.value"/);
});