From 7b4aa23f351c519b18803adf8ffe5ccbeb4a54eb Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Wed, 4 Mar 2026 18:05:00 +0800 Subject: [PATCH] perf(chat): speed up session switch and stabilize message cursor --- electron/services/chatService.ts | 21 +++-- src/pages/ChatPage.tsx | 133 +++++++++++++++++++++---------- src/types/electron.d.ts | 1 + 3 files changed, 104 insertions(+), 51 deletions(-) diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index a639930..df0aa0a 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -1435,6 +1435,7 @@ class ChatService { endTime: number = 0, ascending: boolean = false ): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> { + let releaseMessageCursorMutex: (() => void) | null = null try { const connectResult = await this.ensureConnected() if (!connectResult.success) { @@ -1448,6 +1449,12 @@ class ChatService { await new Promise(resolve => setTimeout(resolve, 1)) } this.messageCursorMutex = true + let mutexReleased = false + releaseMessageCursorMutex = () => { + if (mutexReleased) return + this.messageCursorMutex = false + mutexReleased = true + } let state = this.messageCursors.get(sessionId) @@ -1486,7 +1493,7 @@ class ChatService { state = { cursor: cursorResult.cursor, fetched: 0, batchSize, startTime, endTime, ascending } this.messageCursors.set(sessionId, state) - this.messageCursorMutex = false + releaseMessageCursorMutex?.() // 如果需要跳过消息(offset > 0),逐批获取但不返回 // 注意:仅在 offset === 0 时重建游标最安全; @@ -1519,7 +1526,6 @@ class ChatService { } skipped += count - state.fetched += count // If satisfied offset, break if (skipped >= offset) break; @@ -1532,6 +1538,7 @@ class ChatService { if (attempts >= maxSkipAttempts) { console.error(`[ChatService] 跳过消息超过最大尝试次数: attempts=${attempts}`) } + state.fetched = offset console.log(`[ChatService] 跳过完成: skipped=${skipped}, fetched=${state.fetched}, buffered=${state.bufferedMessages?.length || 0}`) } } @@ -1557,7 +1564,6 @@ class ChatService { const nextBatch = await wcdbService.fetchMessageBatch(state.cursor) if (nextBatch.success && nextBatch.rows) { rows = rows.concat(nextBatch.rows) - state.fetched += nextBatch.rows.length actualHasMore = nextBatch.hasMore === true } else if (!nextBatch.success) { console.error('[ChatService] 获取消息批次失败:', nextBatch.error) @@ -1624,14 +1630,15 @@ class ChatService { } state.fetched += rows.length - this.messageCursorMutex = false + releaseMessageCursorMutex?.() this.messageCacheService.set(sessionId, filtered) return { success: true, messages: filtered, hasMore } } catch (e) { - this.messageCursorMutex = false console.error('ChatService: 获取消息失败:', e) return { success: false, error: String(e) } + } finally { + releaseMessageCursorMutex?.() } } @@ -1726,7 +1733,7 @@ class ChatService { } - async getLatestMessages(sessionId: string, limit: number = this.messageBatchDefault): Promise<{ success: boolean; messages?: Message[]; error?: string }> { + async getLatestMessages(sessionId: string, limit: number = this.messageBatchDefault): Promise<{ success: boolean; messages?: Message[]; hasMore?: boolean; error?: string }> { try { const connectResult = await this.ensureConnected() if (!connectResult.success) { @@ -1757,7 +1764,7 @@ class ChatService { await Promise.allSettled(fixPromises) } - return { success: true, messages: normalized } + return { success: true, messages: normalized, hasMore: batch.hasMore === true } } finally { await wcdbService.closeMessageCursor(cursorResult.cursor) } diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index b0dd9ca..bcc3479 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -290,6 +290,13 @@ interface GroupMembersPanelCacheEntry { includeMessageCounts: boolean } +interface LoadMessagesOptions { + preferLatestPath?: boolean + deferGroupSenderWarmup?: boolean + forceInitialLimit?: number + switchRequestSeq?: number +} + // 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts // 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts import { avatarLoadQueue } from '../utils/AvatarLoadQueue' @@ -521,6 +528,8 @@ function ChatPage(_props: ChatPageProps) { const sessionsRef = useRef([]) const currentSessionRef = useRef(null) const pendingSessionLoadRef = useRef(null) + const sessionSwitchRequestSeqRef = useRef(0) + const initialLoadRequestedSessionRef = useRef(null) const prevSessionRef = useRef(null) const isLoadingMessagesRef = useRef(false) const isLoadingMoreRef = useRef(false) @@ -1424,6 +1433,8 @@ function ChatPage(_props: ChatPageProps) { preloadImageKeysRef.current.clear() lastPreloadSessionRef.current = null pendingSessionLoadRef.current = null + initialLoadRequestedSessionRef.current = null + sessionSwitchRequestSeqRef.current += 1 setIsSessionSwitching(false) setSessionDetail(null) setIsRefreshingDetailStats(false) @@ -1887,32 +1898,61 @@ function ChatPage(_props: ChatPageProps) { setIsRefreshingMessages(false) } } - - - - // 动态游标批量大小控制 + // 消息批量大小控制(保持稳定,避免游标反复重建) const currentBatchSizeRef = useRef(50) + const warmupGroupSenderProfiles = useCallback((usernames: string[], defer = false) => { + if (!Array.isArray(usernames) || usernames.length === 0) return + + const runWarmup = () => { + const batchPromise = loadContactInfoBatch(usernames) + usernames.forEach(username => { + if (!senderAvatarLoading.has(username)) { + senderAvatarLoading.set(username, batchPromise.then(() => senderAvatarCache.get(username) || null)) + } + }) + batchPromise.finally(() => { + usernames.forEach(username => senderAvatarLoading.delete(username)) + }) + } + + if (defer) { + if ('requestIdleCallback' in window) { + window.requestIdleCallback(() => { + runWarmup() + }, { timeout: 1200 }) + } else { + globalThis.setTimeout(runWarmup, 120) + } + return + } + + runWarmup() + }, [loadContactInfoBatch]) + // 加载消息 - const loadMessages = async (sessionId: string, offset = 0, startTime = 0, endTime = 0, ascending = false) => { + const loadMessages = async ( + sessionId: string, + offset = 0, + startTime = 0, + endTime = 0, + ascending = false, + options: LoadMessagesOptions = {} + ) => { const listEl = messageListRef.current const session = sessionMapRef.current.get(sessionId) const unreadCount = session?.unreadCount ?? 0 - let messageLimit = 50 + let messageLimit = currentBatchSizeRef.current if (offset === 0) { - // 初始加载:重置批量大小 - currentBatchSizeRef.current = 50 - // 首屏优化:消息过多时限制数量 - messageLimit = unreadCount > 99 ? 30 : 50 + const preferredLimit = Number.isFinite(options.forceInitialLimit) + ? Math.max(10, Math.floor(options.forceInitialLimit as number)) + : (unreadCount > 99 ? 30 : 40) + currentBatchSizeRef.current = preferredLimit + messageLimit = preferredLimit } else { - // 滚动加载:动态递增 (50 -> 100 -> 200) - if (currentBatchSizeRef.current < 100) { - currentBatchSizeRef.current = 100 - } else { - currentBatchSizeRef.current = 200 - } + // 同一会话内保持固定批量,避免后端游标因 batch 改变而重建 messageLimit = currentBatchSizeRef.current } @@ -1929,12 +1969,19 @@ function ChatPage(_props: ChatPageProps) { const firstMsgEl = listEl?.querySelector('.message-wrapper') as HTMLElement | null try { - const result = await window.electronAPI.chat.getMessages(sessionId, offset, messageLimit, startTime, endTime, ascending) as { + const useLatestPath = offset === 0 && startTime === 0 && endTime === 0 && !ascending && options.preferLatestPath + const result = (useLatestPath + ? await window.electronAPI.chat.getLatestMessages(sessionId, messageLimit) + : await window.electronAPI.chat.getMessages(sessionId, offset, messageLimit, startTime, endTime, ascending) + ) as { success: boolean; messages?: Message[]; hasMore?: boolean; error?: string } + if (options.switchRequestSeq && options.switchRequestSeq !== sessionSwitchRequestSeqRef.current) { + return + } if (currentSessionRef.current !== sessionId) { return } @@ -1947,8 +1994,7 @@ function ChatPage(_props: ChatPageProps) { setHasMoreMessages(false) } - // 预取发送者信息:在关闭加载遮罩前处理 - const unreadCount = session?.unreadCount ?? 0 + // 群聊发送者信息补齐改为非阻塞执行,避免影响首屏切换 const isGroup = sessionId.includes('@chatroom') if (isGroup && result.messages.length > 0) { const unknownSenders = [...new Set(result.messages @@ -1956,18 +2002,7 @@ function ChatPage(_props: ChatPageProps) { .map(m => m.senderUsername as string) )] if (unknownSenders.length > 0) { - - // 在批量请求前,先将这些发送者标记为加载中,防止 MessageBubble 触发重复请求 - const batchPromise = loadContactInfoBatch(unknownSenders) - unknownSenders.forEach(username => { - if (!senderAvatarLoading.has(username)) { - senderAvatarLoading.set(username, batchPromise.then(() => senderAvatarCache.get(username) || null)) - } - }) - // 确保在请求完成后清理 loading 状态 - batchPromise.finally(() => { - unknownSenders.forEach(username => senderAvatarLoading.delete(username)) - }) + warmupGroupSenderProfiles(unknownSenders, options.deferGroupSenderWarmup === true) } } @@ -1993,15 +2028,7 @@ function ChatPage(_props: ChatPageProps) { .map(m => m.senderUsername as string) )] if (unknownSenders.length > 0) { - const batchPromise = loadContactInfoBatch(unknownSenders) - unknownSenders.forEach(username => { - if (!senderAvatarLoading.has(username)) { - senderAvatarLoading.set(username, batchPromise.then(() => senderAvatarCache.get(username) || null)) - } - }) - batchPromise.finally(() => { - unknownSenders.forEach(username => senderAvatarLoading.delete(username)) - }) + warmupGroupSenderProfiles(unknownSenders, false) } } @@ -2042,8 +2069,11 @@ function ChatPage(_props: ChatPageProps) { setLoadingMessages(false) setLoadingMore(false) if (offset === 0 && pendingSessionLoadRef.current === sessionId) { - pendingSessionLoadRef.current = null - setIsSessionSwitching(false) + if (!options.switchRequestSeq || options.switchRequestSeq === sessionSwitchRequestSeqRef.current) { + pendingSessionLoadRef.current = null + initialLoadRequestedSessionRef.current = null + setIsSessionSwitching(false) + } } } } @@ -2088,7 +2118,10 @@ function ChatPage(_props: ChatPageProps) { return } if (session.username === currentSessionId) return + const switchRequestSeq = sessionSwitchRequestSeqRef.current + 1 + sessionSwitchRequestSeqRef.current = switchRequestSeq pendingSessionLoadRef.current = session.username + initialLoadRequestedSessionRef.current = session.username setIsSessionSwitching(true) setCurrentSession(session.username, { preserveMessages: false }) void hydrateSessionPreview(session.username) @@ -2096,7 +2129,12 @@ function ChatPage(_props: ChatPageProps) { setJumpStartTime(0) setJumpEndTime(0) setNoMessageTable(false) - void loadMessages(session.username, 0, 0, 0) + void loadMessages(session.username, 0, 0, 0, false, { + preferLatestPath: true, + deferGroupSenderWarmup: true, + forceInitialLimit: 30, + switchRequestSeq + }) // 切换会话后回到正常聊天窗口:收起详情侧栏,详情需手动再次展开 setShowDetailPanel(false) setShowGroupMembersPanel(false) @@ -2376,8 +2414,15 @@ function ChatPage(_props: ChatPageProps) { useEffect(() => { if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore && !noMessageTable) { + if (pendingSessionLoadRef.current === currentSessionId) return + if (initialLoadRequestedSessionRef.current === currentSessionId) return + initialLoadRequestedSessionRef.current = currentSessionId setHasInitialMessages(false) - loadMessages(currentSessionId, 0) + void loadMessages(currentSessionId, 0, 0, 0, false, { + preferLatestPath: true, + deferGroupSenderWarmup: true, + forceInitialLimit: 30 + }) } }, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore, noMessageTable]) diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index ed4fc9b..0e45244 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -177,6 +177,7 @@ export interface ElectronAPI { getLatestMessages: (sessionId: string, limit?: number) => Promise<{ success: boolean messages?: Message[] + hasMore?: boolean error?: string }> getNewMessages: (sessionId: string, minTime: number, limit?: number) => Promise<{