diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 314b0aa5..e115a7ee 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,26 +22,18 @@ - - ## 关联 Issue - - ## 测试情况 - - ## 截图 / 录屏 - - ## 自查清单 - [ ] 本 PR 只包含**一个主要功能 / 修复**,没有夹带无关改动 diff --git a/electron/main/core/index.ts b/electron/main/core/index.ts index 753adc01..b11e498b 100644 --- a/electron/main/core/index.ts +++ b/electron/main/core/index.ts @@ -44,6 +44,12 @@ const configureMemoryOptimizations = (): void => { const MEMORY_LOG_INTERVAL_MS = 10 * 60 * 1000; /** 启动后首次采样延迟,避开启动期波动 */ const MEMORY_LOG_FIRST_DELAY_MS = 60 * 1000; +/** 启动后自动更新延迟,避免抢占首屏资源 */ +const UPDATER_INIT_DELAY_MS = 2 * 60 * 1000; +/** 主窗口完成加载后恢复歌词辅助窗口,避免任务栏嵌入阻塞首屏 */ +const LYRIC_WINDOWS_RESTORE_DELAY_MS = 1000; +/** 主窗口加载后再初始化系统媒体控制 */ +const MEDIA_INIT_DELAY_MS = 500; /** 记录各进程内存工作集,用于量化内存表现与防劣化对比 */ const logProcessMemory = (): void => { @@ -107,7 +113,11 @@ export const initApp = (): void => { // 注册 IPC registerIpcHandlers(); // 创建主窗口 - createMainWindow(); + const mainWin = createMainWindow(); + mainWin.webContents.once("did-finish-load", () => { + setTimeout(() => initMedia(), MEDIA_INIT_DELAY_MS); + setTimeout(() => restoreLyricWindows(), LYRIC_WINDOWS_RESTORE_DELAY_MS); + }); // 注册 orpheus 协议并处理冷启动唤起 initOrpheusRegistration(); const coldOrpheusUrl = extractOrpheusUrl(process.argv); @@ -118,21 +128,17 @@ export const initApp = (): void => { void initSongCache(); // 启动下载服务 void initDownload(); - initMedia(); // 初始化 Last.fm 集成 initLastfm(); // 初始化插件系统 pluginRegistry.init(); // 初始化播放事件桥(需在 pluginRegistry.init 之后,读 hasEnabledControlPlugin) initPlaybackBridge(); - // 恢复歌词相关窗口 - restoreLyricWindows(); // 注册全局快捷键 initGlobalHotkey(); // 启动外部 API 服务 void startServer(); - // 初始化自动更新 - initUpdater(); + setTimeout(initUpdater, UPDATER_INIT_DELAY_MS); // 周期记录各进程内存 setTimeout(logProcessMemory, MEMORY_LOG_FIRST_DELAY_MS); setInterval(logProcessMemory, MEMORY_LOG_INTERVAL_MS); diff --git a/electron/main/window/main.ts b/electron/main/window/main.ts index 4cfeb395..e9e05fa6 100644 --- a/electron/main/window/main.ts +++ b/electron/main/window/main.ts @@ -9,6 +9,7 @@ import { store } from "@main/store"; import { handleCacheProtocolOnPartition } from "@main/utils/protocol"; import { isAppQuitting } from "@main/utils/lifecycle"; import { broadcast } from "@main/utils/broadcast"; +import { coreLog } from "@main/utils/logger"; /** 主窗口 session */ const MAIN_PARTITION = "persist:main"; @@ -26,6 +27,7 @@ let mainWindow: BrowserWindow | null = null; * 创建主窗口 */ export const createMainWindow = (): BrowserWindow => { + const createdAt = Date.now(); const remember = store.get("system.rememberWindowState") ?? true; const saved = remember ? store.get("windowStates.main") : undefined; @@ -51,14 +53,21 @@ export const createMainWindow = (): BrowserWindow => { // 初始化托盘 initTray(); - // 自定义任务栏缩略图 - enableTaskbarThumbnail(mainWindow); - // 缩略图工具栏 mainWindow.webContents.once("did-finish-load", () => { + coreLog.info(`主窗口 did-finish-load,用时 ${Date.now() - createdAt}ms`); + enableTaskbarThumbnail(mainWindow!); initThumbar(mainWindow!); }); + mainWindow.webContents.once("dom-ready", () => { + coreLog.info(`主窗口 dom-ready,用时 ${Date.now() - createdAt}ms`); + }); + + mainWindow.webContents.once("did-fail-load", (_event, errorCode, errorDescription) => { + coreLog.warn(`主窗口加载失败 ${errorCode}: ${errorDescription}`); + }); + // 每次加载完成应用界面缩放 mainWindow.webContents.on("did-finish-load", () => { applyMainWindowZoom(); diff --git a/scripts/build-native.ts b/scripts/build-native.ts index 8e19a769..dd3956a8 100644 --- a/scripts/build-native.ts +++ b/scripts/build-native.ts @@ -1,4 +1,5 @@ import { spawnSync } from "node:child_process"; +import { join } from "node:path"; import process from "node:process"; interface NativeModule { @@ -80,6 +81,9 @@ const parseArgs = () => { const napiArgs = ["--no-const-enum"]; const options = parseArgs(); +const buildType = options.isDev ? "debug" : "release"; +const projectRoot = process.cwd(); +const napiCliPath = join(projectRoot, "node_modules", "@napi-rs", "cli", "dist", "cli.js"); if (!options.isDev) napiArgs.push("--release"); if (options.passing) napiArgs.push(...options.passing); @@ -90,12 +94,11 @@ for (const mod of modules) { } const cwd = `native/${mod.name}`; - const buildType = options.isDev ? "debug" : "release"; console.log(`[BuildNative] 构建 ${mod.name} (${buildType})`); - const result = spawnSync("napi", ["build", ...napiArgs], { + const result = spawnSync(process.execPath, [napiCliPath, "build", ...napiArgs], { stdio: "inherit", - shell: process.platform === "win32", + shell: false, cwd, }); diff --git a/src/core/player/index.ts b/src/core/player/index.ts index 993902d8..734cbabc 100644 --- a/src/core/player/index.ts +++ b/src/core/player/index.ts @@ -805,22 +805,31 @@ export const moveInQueue = (fromIndex: number, toIndex: number): void => { let unsubscribe: (() => void) | null = null; let initialized = false; +const logInitStep = (startedAt: number, step: string): void => { + console.info(`[player] init ${step} ${Math.round(performance.now() - startedAt)}ms`); +}; + /** 初始化播放器 */ export const initPlayer = async (): Promise => { if (initialized) return; initialized = true; - console.log("[player] init"); + const startedAt = performance.now(); + console.info("[player] init"); // 先从主进程同步后端配置,确保 system 设置可用 const settings = useSettingsStore(); await settings.syncSystem(); + logInitStep(startedAt, "settings synced"); // 流媒体 store 必须在恢复队列前就绪,否则队列里的 streaming track 拿不到 cfg await useStreamingStore().init(); + logInitStep(startedAt, "streaming initialized"); // 插件 store 同理:在线歌曲 URL 兜底走插件,列表必须在 loadTrack 前就绪 void usePluginsStore().load(); await queue.restoreQueue(); + logInitStep(startedAt, "queue restored"); const status = useStatusStore(); // 恢复上次的音量和播放模式到主进程 await window.api.player.setVolume(status.volume); + logInitStep(startedAt, "player backend ready"); syncPlayMode(); // 应用渐入渐出配置 const { fadeEnabled, fadeDuration, loudnessNormalization, equalizer } = settings.system.player; @@ -835,6 +844,7 @@ export const initPlayer = async (): Promise => { } // 刷新设备列表并恢复上次选择的输出设备 await refreshDevices(); + logInitStep(startedAt, "devices refreshed"); if (settings.player.outputDevice) { await window.api.player.setOutputDevice(settings.player.outputDevice); } @@ -883,6 +893,7 @@ export const initPlayer = async (): Promise => { } else { status.state = "idle"; } + logInitStep(startedAt, "done"); }; /** 清理事件订阅 */ diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index ff8f275c..be375def 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -4,6 +4,17 @@ import { useMediaStore } from "@/stores/media"; import { useSettingsStore } from "@/stores/settings"; import { useOrpheusProtocol } from "@/composables/useOrpheusProtocol"; +const PlayerBar = defineAsyncComponent(() => import("@/components/player/PlayerBar.vue")); +const FullPlayer = defineAsyncComponent(() => import("@/components/player/FullPlayer/index.vue")); +const SettingsDialog = defineAsyncComponent( + () => import("@/components/settings/SettingsDialog.vue"), +); +const UpdateDialog = defineAsyncComponent(() => import("@/components/modals/UpdateDialog.vue")); +const SPerformanceMonitor = defineAsyncComponent( + () => import("@/components/SPerformanceMonitor.vue"), +); +const SDialogProvider = defineAsyncComponent(() => import("@/components/ui/SDialogProvider.vue")); + const route = useRoute(); const status = useStatusStore(); const settings = useSettingsStore(); diff --git a/src/main.ts b/src/main.ts index 6ef9d405..a81eb367 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,11 @@ import { initPlayer } from "./core/player"; import { installHotkeyManager } from "./core/hotkey/manager"; import { vRipple } from "./directives/ripple"; +const bootStart = performance.now(); +const logBoot = (stage: string): void => { + console.info(`[boot] ${stage} ${Math.round(performance.now() - bootStart)}ms`); +}; + const pinia = createPinia(); pinia.use(piniaPersistedstate); @@ -40,8 +45,10 @@ const SPLASH_ANIM_MS = 2050; // 初始化程序 router.isReady().then(() => { + logBoot("router ready"); // 挂载应用 app.mount("#app"); + logBoot("app mounted"); // 笔画播完即淡出 const remaining = Math.max(0, SPLASH_ANIM_MS - performance.now()); setTimeout(() => { @@ -50,9 +57,12 @@ router.isReady().then(() => { loading.classList.add("hidden"); loading.addEventListener("transitionend", () => loading.remove(), { once: true }); } + logBoot("splash hidden"); }, remaining); // 初始化播放器 - initPlayer().catch(console.error); + initPlayer() + .then(() => logBoot("player ready")) + .catch(console.error); // 初始化快捷键 useHotkeyStore() .init() diff --git a/src/stores/streaming.ts b/src/stores/streaming.ts index 713f531c..8a3b4a71 100644 --- a/src/stores/streaming.ts +++ b/src/stores/streaming.ts @@ -52,6 +52,8 @@ export const useStreamingStore = defineStore("streaming", () => { const playlists = shallowRef([]); /** 缓存最后更新时间(ms) */ const lastFetchedAt = ref(0); + /** 是否已加载服务器配置 */ + const initialized = ref(false); /** 是否已从 IndexedDB 完成首次水合 */ const hydrated = ref(false); @@ -568,14 +570,15 @@ export const useStreamingStore = defineStore("streaming", () => { }; const init = async (): Promise => { - if (hydrated.value) return; + if (initialized.value) return; + initialized.value = true; const result = await window.api.streaming.loadServers(); servers.value = result.servers; activeServerId.value = result.activeServerId; if (activeServerId.value && !servers.value.find((s) => s.id === activeServerId.value)) { activeServerId.value = null; } - await hydrateFromCache(); + void hydrateFromCache(); if (activeServerId.value) void connectToServer(activeServerId.value); }; diff --git a/src/stores/update.ts b/src/stores/update.ts index 28c75fa5..90445361 100644 --- a/src/stores/update.ts +++ b/src/stores/update.ts @@ -4,6 +4,9 @@ import i18n from "@/i18n"; const { t } = i18n.global; +/** 启动后自动检查更新延迟,避免抢占首屏加载 */ +const AUTO_CHECK_DELAY_MS = 2 * 60 * 1000; + export const useUpdateStore = defineStore("update", () => { /** 当前阶段 */ const phase = ref("idle"); @@ -56,8 +59,10 @@ export const useUpdateStore = defineStore("update", () => { // 订阅主进程推送的更新事件 const unsubscribe = window.api.update.onEvent(handleEvent); onScopeDispose(unsubscribe); - // 触发启动检查 - void window.api.update.check(false); + const autoCheckTimer = window.setTimeout(() => { + void window.api.update.check(false); + }, AUTO_CHECK_DELAY_MS); + onScopeDispose(() => window.clearTimeout(autoCheckTimer)); /** 手动检查更新 */ const checkManually = (): void => {