From b6be8a74df757c4cb1c11e8e685f94c585c7efc2 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 18 May 2026 23:19:30 +0800 Subject: [PATCH] perf(desktop): defer startup payloads --- .../src/components/layout/ContentArea.vue | 32 ++++++++++++---- apps/desktop/src/i18n/index.ts | 37 +++++++++++++++---- apps/desktop/src/main.ts | 18 ++++++--- .../app-tests/startupChunkBoundaries.test.ts | 21 +++++++++++ 4 files changed, 87 insertions(+), 21 deletions(-) diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index ee906b0068..a45d67db0a 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -18,12 +18,12 @@ import "splitpanes/dist/splitpanes.css"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import QueryEditor from "@/components/editor/QueryEditor.vue"; -import DataGrid from "@/components/grid/DataGrid.vue"; -import RedisKeyBrowser from "@/components/redis/RedisKeyBrowser.vue"; -import MongoDocBrowser from "@/components/mongo/MongoDocBrowser.vue"; -import ObjectBrowser from "@/components/objects/ObjectBrowser.vue"; import ColumnInfoPanel from "@/components/editor/ColumnInfoPanel.vue"; import type { ColumnInfo } from "@/components/editor/ColumnInfoPanel.vue"; +const DataGrid = defineAsyncComponent(() => import("@/components/grid/DataGrid.vue")); +const RedisKeyBrowser = defineAsyncComponent(() => import("@/components/redis/RedisKeyBrowser.vue")); +const MongoDocBrowser = defineAsyncComponent(() => import("@/components/mongo/MongoDocBrowser.vue")); +const ObjectBrowser = defineAsyncComponent(() => import("@/components/objects/ObjectBrowser.vue")); const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explain/ExplainPlanViewer.vue")); const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue")); import { useQueryStore } from "@/stores/queryStore"; @@ -33,6 +33,24 @@ import { isTableDataEditable } from "@/lib/tableEditing"; import type { QueryTab, ConnectionConfig } from "@/types/database"; import type { SqlFormatDialect } from "@/lib/sqlFormatter"; +type DataGridHandle = { + onToolbarRefresh: () => Promise | void; + focusSearch: () => boolean; + visibleColumnCount: number; + displayableColumnCount: number; + hiddenColumnCount: number; + filteredColumnVisibilityOptions: (search: string) => Array<{ index: number; column: string }>; + isColumnVisible: (columnIndex: number) => boolean; + toggleColumnVisibility: (columnIndex: number) => void; + showAllColumns: () => void; + showDdl: boolean; + toggleDdl: () => void; +}; + +type SearchableBrowserHandle = { + focusSearch: () => boolean; +}; + const props = defineProps<{ activeTab: QueryTab; activeConnection?: ConnectionConfig; @@ -71,13 +89,13 @@ const showColumnInfo = ref(false); const columnInfoColumns = ref([]); const columnInfoLoading = ref(false); const columnInfoError = ref(undefined); -const dataGridRef = ref>(); +const dataGridRef = ref(); const columnVisibilitySearch = ref(""); const columnVisibilityOptions = computed( () => dataGridRef.value?.filteredColumnVisibilityOptions(columnVisibilitySearch.value) ?? [], ); -const redisKeyBrowserRef = ref>(); -const objectBrowserRef = ref>(); +const redisKeyBrowserRef = ref(); +const objectBrowserRef = ref(); const activeSqlFormatDialect = computed(() => { switch (props.activeConnection?.db_type) { diff --git a/apps/desktop/src/i18n/index.ts b/apps/desktop/src/i18n/index.ts index 6a968a3cb4..1bb5b0161a 100644 --- a/apps/desktop/src/i18n/index.ts +++ b/apps/desktop/src/i18n/index.ts @@ -1,12 +1,20 @@ import { createI18n } from "vue-i18n"; -import en from "./locales/en"; -import es from "./locales/es"; import zhCN from "./locales/zh-CN"; export type Locale = "en" | "es" | "zh-CN"; +type LocaleMessages = Record; +type I18nGlobal = { + locale: { value: Locale }; + setLocaleMessage: (locale: Locale, messages: LocaleMessages) => void; +}; const supportedLocales: Locale[] = ["en", "es", "zh-CN"]; const defaultLocale: Locale = "zh-CN"; +const loadedLocales = new Set([defaultLocale]); +const localeLoaders: Record, () => Promise<{ default: LocaleMessages }>> = { + en: () => import("./locales/en"), + es: () => import("./locales/es"), +}; function normalizeLocale(value: string | null): Locale { if (value && supportedLocales.includes(value as Locale)) { @@ -20,21 +28,34 @@ const savedLocale = normalizeLocale(localStorage.getItem("dbx-locale")); const i18n = createI18n({ legacy: false, locale: savedLocale, - fallbackLocale: "en", + fallbackLocale: defaultLocale, messages: { - en, - es, "zh-CN": zhCN, }, }); +const i18nGlobal = i18n.global as unknown as I18nGlobal; -export function setLocale(locale: Locale) { - i18n.global.locale.value = locale; +export async function loadLocaleMessages(locale: Locale) { + if (loadedLocales.has(locale)) return; + const loader = localeLoaders[locale as Exclude]; + if (!loader) return; + const messages = await loader(); + i18nGlobal.setLocaleMessage(locale, messages.default); + loadedLocales.add(locale); +} + +export async function loadSavedLocale() { + await loadLocaleMessages(savedLocale); +} + +export async function setLocale(locale: Locale) { + await loadLocaleMessages(locale); + i18nGlobal.locale.value = locale; localStorage.setItem("dbx-locale", locale); } export function currentLocale(): Locale { - return i18n.global.locale.value as Locale; + return i18nGlobal.locale.value; } export function nextLocale(current: Locale): Locale { diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 3f7da5cddb..4985e80a6e 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -2,12 +2,18 @@ import { createApp } from "vue"; import { createPinia } from "pinia"; import VueVirtualScroller from "vue-virtual-scroller"; import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; -import i18n from "./i18n"; +import i18n, { loadSavedLocale } from "./i18n"; import App from "./App.vue"; import "./styles/globals.css"; -const app = createApp(App); -app.use(createPinia()); -app.use(i18n); -app.use(VueVirtualScroller); -app.mount("#root"); +async function bootstrap() { + await loadSavedLocale(); + + const app = createApp(App); + app.use(createPinia()); + app.use(i18n); + app.use(VueVirtualScroller); + app.mount("#root"); +} + +void bootstrap(); diff --git a/packages/app-tests/startupChunkBoundaries.test.ts b/packages/app-tests/startupChunkBoundaries.test.ts index 25af7afe7b..69a033a132 100644 --- a/packages/app-tests/startupChunkBoundaries.test.ts +++ b/packages/app-tests/startupChunkBoundaries.test.ts @@ -4,6 +4,9 @@ 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"); +const contentAreaSource = readFileSync("apps/desktop/src/components/layout/ContentArea.vue", "utf8"); +const i18nSource = readFileSync("apps/desktop/src/i18n/index.ts", "utf8"); +const mainSource = readFileSync("apps/desktop/src/main.ts", "utf8"); test("app defers cold-start side panels and modal pages behind async components", () => { assert.match(appSource, /defineAsyncComponent/); @@ -28,3 +31,21 @@ test("app dialogs only render async dialogs when their open state needs them", ( assert.match(appDialogsSource, / { + assert.match(contentAreaSource, /defineAsyncComponent/); + for (const component of ["DataGrid", "RedisKeyBrowser", "MongoDocBrowser", "ObjectBrowser"]) { + assert.doesNotMatch(contentAreaSource, new RegExp(`import ${component} from`)); + assert.match(contentAreaSource, new RegExp(`const ${component} = defineAsyncComponent`)); + } +}); + +test("i18n defers non-default locale payloads out of the startup chunk", () => { + assert.doesNotMatch(i18nSource, /import en from "\.\/locales\/en"/); + assert.doesNotMatch(i18nSource, /import es from "\.\/locales\/es"/); + assert.match(i18nSource, /const localeLoaders/); + assert.match(i18nSource, /en:\s*\(\)\s*=>\s*import\("\.\/locales\/en"\)/); + assert.match(i18nSource, /es:\s*\(\)\s*=>\s*import\("\.\/locales\/es"\)/); + assert.match(mainSource, /loadSavedLocale\(\)/); + assert.match(mainSource, /app\.mount\("#root"\)/); +});